Files
FunguyBot/plugins/ddg.py
T

346 lines
12 KiB
Python
Raw 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.
#!/usr/bin/env python3
"""
DuckDuckGo search plugin (ddgs library). Results are shown inside collapsible details boxes.
"""
import asyncio
import logging
from html import escape
import simplematrixbotlib as botlib
from ddgs import DDGS
logger = logging.getLogger("ddg")
# ---------------------------------------------------------------------------
# Async search wrapper
# ---------------------------------------------------------------------------
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")):
return
args = match.args()
if not args:
await send_help(room, bot)
return
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."""
try:
with DDGS() as ddgs:
results = await _async_search(ddgs.text, query, max_results=1)
except Exception as e:
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."
)
return
content = ""
if results:
r = results[0]
title = escape(r.get("title", "Result"))
body = 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)}"
content = f"No results found.<br>🔍 <a href='{search_url}'>Search on DuckDuckGo</a>"
msg = f"""<details>
<summary>🦆 DuckDuckGo: {escape(query)}</summary>
{content}
</details>"""
await bot.api.send_markdown_message(room.room_id, msg)
async def web_search(room, bot, query):
try:
with DDGS() as ddgs:
results = await _async_search(ddgs.text, query, max_results=5)
except Exception as e:
logger.error(f"DDG web search error: {e}")
await bot.api.send_text_message(room.room_id, f"Error: {e}")
return
if not results:
await bot.api.send_text_message(room.room_id, f"No results for '{query}'.")
return
items = ""
for r in results:
title = escape(r.get("title", "Result"))
body = 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>"""
await bot.api.send_markdown_message(room.room_id, msg)
async def image_search(room, bot, query):
try:
with DDGS() as ddgs:
results = await _async_search(ddgs.images, query, max_results=3)
except Exception as e:
logger.error(f"DDG image error: {e}")
await bot.api.send_text_message(room.room_id, f"Error: {e}")
return
if not results:
await bot.api.send_text_message(room.room_id, f"No images for '{query}'.")
return
items = ""
for img in results:
title = 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"
items += f"<br>🔍 <a href='{search_url}'>View all images</a>"
msg = f"""<details>
<summary>🖼️ Images: {escape(query)}</summary>
{items}
</details>"""
await bot.api.send_markdown_message(room.room_id, msg)
async def news_search(room, bot, query):
try:
with DDGS() as ddgs:
results = await _async_search(ddgs.news, query, max_results=3)
except Exception as e:
logger.error(f"DDG news error: {e}")
await bot.api.send_text_message(room.room_id, f"Error: {e}")
return
if not results:
await bot.api.send_text_message(room.room_id, f"No news for '{query}'.")
return
items = ""
for n in results:
title = escape(n.get("title", "Article"))
body = 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>"""
await bot.api.send_markdown_message(room.room_id, msg)
async def video_search(room, bot, query):
try:
with DDGS() as ddgs:
results = await _async_search(ddgs.videos, query, max_results=3)
except Exception as e:
logger.error(f"DDG video error: {e}")
await bot.api.send_text_message(room.room_id, f"Error: {e}")
return
if not results:
await bot.api.send_text_message(room.room_id, f"No videos for '{query}'.")
return
items = ""
for v in results:
title = 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"
items += f"<br>🔍 <a href='{search_url}'>View all videos</a>"
msg = f"""<details>
<summary>🎬 Videos: {escape(query)}</summary>
{items}
</details>"""
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>"""
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>
Usage: <code>!ddg bang !bang query</code><br><br>
<strong>Popular bangs:</strong><br>
• <code>!w</code> Wikipedia
• <code>!g</code> Google
• <code>!yt</code> YouTube
• <code>!aw</code> ArchWiki
• <code>!gh</code> GitHub
• <code>!so</code> Stack Overflow
• <code>!reddit</code> Reddit
<br>
<a href="https://duckduckgo.com/bangs">Full list here</a>
"""
await bot.api.send_markdown_message(room.room_id, msg)
async def send_help(room, bot):
help_msg = """
<strong>🦆 DuckDuckGo Commands</strong><br>
<code>!ddg &lt;query&gt;</code> Top result (collapsible)<br>
<code>!ddg search &lt;query&gt;</code> 5 web results<br>
<code>!ddg image &lt;query&gt;</code> 3 images<br>
<code>!ddg news &lt;query&gt;</code> 3 news articles<br>
<code>!ddg video &lt;query&gt;</code> 3 videos<br>
<code>!ddg bang &lt;!bang query&gt;</code> Bang redirect<br>
<code>!ddg define &lt;word&gt;</code> Definition<br>
<code>!ddg calc &lt;expr&gt;</code> Calculator<br>
<code>!ddg weather [city]</code> Weather<br>
<code>!ddg help</code> This help
"""
await bot.api.send_markdown_message(room.room_id, help_msg)
# ---------------------------------------------------------------------------
# Plugin metadata
# ---------------------------------------------------------------------------
__version__ = "2.1.0"
__author__ = "Funguy Bot"
__description__ = "DuckDuckGo search collapsible results (ddgs library, no API key)"
__help__ = """
<details>
<summary><strong>!ddg</strong> DuckDuckGo search (web, images, news, etc.)</summary>
<ul>
<li><code>!ddg &lt;query&gt;</code> Top web result snippet (collapsible)</li>
<li><code>!ddg search &lt;query&gt;</code> 5 web results</li>
<li><code>!ddg image &lt;query&gt;</code> 3 images</li>
<li><code>!ddg news &lt;query&gt;</code> 3 news articles</li>
<li><code>!ddg video &lt;query&gt;</code> 3 videos</li>
<li><code>!ddg bang &lt;!bang query&gt;</code> Bang redirect</li>
<li><code>!ddg define &lt;word&gt;</code> Definition</li>
<li><code>!ddg calc &lt;expression&gt;</code> Calculator</li>
<li><code>!ddg weather [location]</code> Weather</li>
</ul>
<p>Uses <code>ddgs</code> library. No API key required.</p>
</details>
"""