Files
FunguyBot/plugins/ddg.py
T

286 lines
11 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
from plugins.common import html_escape, collapsible_summary
logger = logging.getLogger("ddg")
# 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))
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()
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)
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)
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)
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)
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)
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)
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)
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)
elif subcommand == "weather":
location = " ".join(args[1:]) if len(args) > 1 else ""
if not location:
location = "current location"
await weather(room, bot, location)
elif subcommand == "help":
await send_help(room, bot)
else:
query = " ".join(args)
await instant_answer(room, bot, query)
async def instant_answer(room, bot, query):
safe_query = html_escape(query)
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: {safe_query}</strong><br><br>Error fetching results."
)
return
content = ""
if results:
r = results[0]
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={html_escape(query)}"
content = f"No results found.<br>🔍 <a href='{search_url}'>Search on DuckDuckGo</a>"
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)
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 '{safe_query}'.")
return
items = ""
for r in results:
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 = 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)
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 '{safe_query}'.")
return
items = ""
for img in results:
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={html_escape(query)}&iax=images&ia=images"
items += f"<br>🔍 <a href='{search_url}'>View all images</a>"
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)
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 '{safe_query}'.")
return
items = ""
for n in results:
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 = 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)
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 '{safe_query}'.")
return
items = ""
for v in results:
title = html_escape(v.get("title", "Video"))
items += f"• <a href='{v['content']}'>{title}</a><br>"
search_url = f"https://duckduckgo.com/?q={html_escape(query)}&iar=videos"
items += f"<br>🔍 <a href='{search_url}'>View all videos</a>"
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):
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}")
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)
__version__ = "2.1.1"
__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>
"""