refactor: async I/O, input sanitisation, and shared utilities cleanup
This commit is contained in:
+33
-93
@@ -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__ = """
|
||||
|
||||
Reference in New Issue
Block a user