refactor: async I/O, input sanitisation, and shared utilities cleanup

This commit is contained in:
2026-05-08 22:59:31 -05:00
parent 52a9621d50
commit f822d6a450
21 changed files with 1351 additions and 2709 deletions
+33 -93
View File
@@ -9,19 +9,15 @@ from html import escape
import simplematrixbotlib as botlib
from ddgs import DDGS
from plugins.common import html_escape, collapsible_summary
logger = logging.getLogger("ddg")
# ---------------------------------------------------------------------------
# Async search wrapper
# ---------------------------------------------------------------------------
# Async search wrapper (ddgs is sync, run in executor)
async def _async_search(func, *args, **kwargs):
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, lambda: func(*args, **kwargs))
# ---------------------------------------------------------------------------
# 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("ddg")):
@@ -34,93 +30,67 @@ async def handle_command(room, message, bot, prefix, config):
subcommand = args[0].lower()
# ---- Instant answer (default) ----
if subcommand in ("instant", "i"):
query = " ".join(args[1:]) if len(args) > 1 else ""
if not query:
await bot.api.send_text_message(room.room_id, "Usage: !ddg instant <query>")
return
await instant_answer(room, bot, query)
# ---- Web search ----
elif subcommand == "search":
query = " ".join(args[1:]) if len(args) > 1 else ""
if not query:
await bot.api.send_text_message(room.room_id, "Usage: !ddg search <query>")
return
await web_search(room, bot, query)
# ---- Image search ----
elif subcommand == "image":
query = " ".join(args[1:]) if len(args) > 1 else ""
if not query:
await bot.api.send_text_message(room.room_id, "Usage: !ddg image <query>")
return
await image_search(room, bot, query)
# ---- News search ----
elif subcommand == "news":
query = " ".join(args[1:]) if len(args) > 1 else ""
if not query:
await bot.api.send_text_message(room.room_id, "Usage: !ddg news <query>")
return
await news_search(room, bot, query)
# ---- Video search ----
elif subcommand == "video":
query = " ".join(args[1:]) if len(args) > 1 else ""
if not query:
await bot.api.send_text_message(room.room_id, "Usage: !ddg video <query>")
return
await video_search(room, bot, query)
# ---- Bang search ----
elif subcommand == "bang":
bang_query = " ".join(args[1:]) if len(args) > 1 else ""
if not bang_query:
await bang_help(room, bot)
return
await bang_search(room, bot, bang_query)
# ---- Definitions ----
elif subcommand == "define":
word = " ".join(args[1:]) if len(args) > 1 else ""
if not word:
await bot.api.send_text_message(room.room_id, "Usage: !ddg define <word>")
return
await definition(room, bot, word)
# ---- Calculator ----
elif subcommand == "calc":
expr = " ".join(args[1:]) if len(args) > 1 else ""
if not expr:
await bot.api.send_text_message(room.room_id, "Usage: !ddg calc <expression>")
return
await calculator(room, bot, expr)
# ---- Weather ----
elif subcommand == "weather":
location = " ".join(args[1:]) if len(args) > 1 else ""
if not location:
location = "current location"
await weather(room, bot, location)
# ---- Help ----
elif subcommand == "help":
await send_help(room, bot)
# ---- Default: treat as instant answer ----
else:
query = " ".join(args)
await instant_answer(room, bot, query)
# ==============================
# Result functions (all wrapped in <details>)
# ==============================
async def instant_answer(room, bot, query):
"""Top web result wrapped in a collapsible box."""
safe_query = html_escape(query)
try:
with DDGS() as ddgs:
results = await _async_search(ddgs.text, query, max_results=1)
@@ -128,28 +98,25 @@ async def instant_answer(room, bot, query):
logger.error(f"DDG instant answer error: {e}")
await bot.api.send_markdown_message(
room.room_id,
f"🦆 <strong>DuckDuckGo: {escape(query)}</strong><br><br>Error fetching results. Try again later."
f"🦆 <strong>DuckDuckGo: {safe_query}</strong><br><br>Error fetching results."
)
return
content = ""
if results:
r = results[0]
title = escape(r.get("title", "Result"))
body = escape(r.get("body", ""))
title = html_escape(r.get("title", "Result"))
body = html_escape(r.get("body", ""))
content = f"💡 <strong>{title}</strong><br>{body[:300]}…<br><a href='{r['href']}'>Read more</a>"
else:
search_url = f"https://duckduckgo.com/?q={escape(query)}"
search_url = f"https://duckduckgo.com/?q={html_escape(query)}"
content = f"No results found.<br>🔍 <a href='{search_url}'>Search on DuckDuckGo</a>"
msg = f"""<details>
<summary>🦆 DuckDuckGo: {escape(query)}</summary>
{content}
</details>"""
msg = collapsible_summary(f"🦆 DuckDuckGo: {safe_query}", content)
await bot.api.send_markdown_message(room.room_id, msg)
async def web_search(room, bot, query):
safe_query = html_escape(query)
try:
with DDGS() as ddgs:
results = await _async_search(ddgs.text, query, max_results=5)
@@ -159,23 +126,20 @@ async def web_search(room, bot, query):
return
if not results:
await bot.api.send_text_message(room.room_id, f"No results for '{query}'.")
await bot.api.send_text_message(room.room_id, f"No results for '{safe_query}'.")
return
items = ""
for r in results:
title = escape(r.get("title", "Result"))
body = escape(r.get("body", ""))
title = html_escape(r.get("title", "Result"))
body = html_escape(r.get("body", ""))
items += f"• <a href='{r['href']}'>{title}</a><br> {body[:200]}…<br><br>"
msg = f"""<details>
<summary>🔍 Search: {escape(query)}</summary>
{items}
</details>"""
msg = collapsible_summary(f"🔍 Search: {safe_query}", items)
await bot.api.send_markdown_message(room.room_id, msg)
async def image_search(room, bot, query):
safe_query = html_escape(query)
try:
with DDGS() as ddgs:
results = await _async_search(ddgs.images, query, max_results=3)
@@ -185,28 +149,25 @@ async def image_search(room, bot, query):
return
if not results:
await bot.api.send_text_message(room.room_id, f"No images for '{query}'.")
await bot.api.send_text_message(room.room_id, f"No images for '{safe_query}'.")
return
items = ""
for img in results:
title = escape(img.get("title", "Image"))
title = html_escape(img.get("title", "Image"))
items += f"• <a href='{img['image']}'>{title}</a>"
if img.get("width") and img.get("height"):
items += f" ({img['width']}×{img['height']})"
items += "<br>"
search_url = f"https://duckduckgo.com/?q={escape(query)}&iax=images&ia=images"
search_url = f"https://duckduckgo.com/?q={html_escape(query)}&iax=images&ia=images"
items += f"<br>🔍 <a href='{search_url}'>View all images</a>"
msg = f"""<details>
<summary>🖼️ Images: {escape(query)}</summary>
{items}
</details>"""
msg = collapsible_summary(f"🖼️ Images: {safe_query}", items)
await bot.api.send_markdown_message(room.room_id, msg)
async def news_search(room, bot, query):
safe_query = html_escape(query)
try:
with DDGS() as ddgs:
results = await _async_search(ddgs.news, query, max_results=3)
@@ -216,23 +177,20 @@ async def news_search(room, bot, query):
return
if not results:
await bot.api.send_text_message(room.room_id, f"No news for '{query}'.")
await bot.api.send_text_message(room.room_id, f"No news for '{safe_query}'.")
return
items = ""
for n in results:
title = escape(n.get("title", "Article"))
body = escape(n.get("body", ""))
title = html_escape(n.get("title", "Article"))
body = html_escape(n.get("body", ""))
items += f"• <a href='{n['url']}'>{title}</a><br> {body[:200]}…<br><br>"
msg = f"""<details>
<summary>📰 News: {escape(query)}</summary>
{items}
</details>"""
msg = collapsible_summary(f"📰 News: {safe_query}", items)
await bot.api.send_markdown_message(room.room_id, msg)
async def video_search(room, bot, query):
safe_query = html_escape(query)
try:
with DDGS() as ddgs:
results = await _async_search(ddgs.videos, query, max_results=3)
@@ -242,49 +200,36 @@ async def video_search(room, bot, query):
return
if not results:
await bot.api.send_text_message(room.room_id, f"No videos for '{query}'.")
await bot.api.send_text_message(room.room_id, f"No videos for '{safe_query}'.")
return
items = ""
for v in results:
title = escape(v.get("title", "Video"))
title = html_escape(v.get("title", "Video"))
items += f"• <a href='{v['content']}'>{title}</a><br>"
search_url = f"https://duckduckgo.com/?q={escape(query)}&iar=videos"
search_url = f"https://duckduckgo.com/?q={html_escape(query)}&iar=videos"
items += f"<br>🔍 <a href='{search_url}'>View all videos</a>"
msg = f"""<details>
<summary>🎬 Videos: {escape(query)}</summary>
{items}
</details>"""
msg = collapsible_summary(f"🎬 Videos: {safe_query}", items)
await bot.api.send_markdown_message(room.room_id, msg)
async def bang_search(room, bot, bang_query):
search_url = f"https://duckduckgo.com/?q={escape(bang_query)}"
content = f"🔗 <a href='{search_url}'>Search with {escape(bang_query)} on DuckDuckGo</a>"
msg = f"""<details>
<summary>🎯 Bang: {escape(bang_query)}</summary>
{content}
</details>"""
safe_query = html_escape(bang_query)
search_url = f"https://duckduckgo.com/?q={html_escape(bang_query)}"
content = f"🔗 <a href='{search_url}'>Search with {safe_query} on DuckDuckGo</a>"
msg = collapsible_summary(f"🎯 Bang: {safe_query}", content)
await bot.api.send_markdown_message(room.room_id, msg)
async def definition(room, bot, word):
await instant_answer(room, bot, f"define {word}")
async def calculator(room, bot, expr):
await instant_answer(room, bot, expr)
async def weather(room, bot, location):
await instant_answer(room, bot, f"weather {location}")
# ---------------------------------------------------------------------------
# Help messages (no details wrapper kept readable)
# ---------------------------------------------------------------------------
async def bang_help(room, bot):
msg = """
<strong>🎯 DuckDuckGo Bangs</strong><br>
@@ -302,7 +247,6 @@ Usage: <code>!ddg bang !bang query</code><br><br>
"""
await bot.api.send_markdown_message(room.room_id, msg)
async def send_help(room, bot):
help_msg = """
<strong>🦆 DuckDuckGo Commands</strong><br>
@@ -319,11 +263,7 @@ async def send_help(room, bot):
"""
await bot.api.send_markdown_message(room.room_id, help_msg)
# ---------------------------------------------------------------------------
# Plugin metadata
# ---------------------------------------------------------------------------
__version__ = "2.1.0"
__version__ = "2.1.1"
__author__ = "Funguy Bot"
__description__ = "DuckDuckGo search collapsible results (ddgs library, no API key)"
__help__ = """