#!/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 ") 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 ") 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 ") 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 ") 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 ") 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 ") 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 ") 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
) # ============================== 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"🦆 DuckDuckGo: {escape(query)}

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"💡 {title}
{body[:300]}…
Read more" else: search_url = f"https://duckduckgo.com/?q={escape(query)}" content = f"No results found.
🔍 Search on DuckDuckGo" msg = f"""
🦆 DuckDuckGo: {escape(query)} {content}
""" 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"• {title}
{body[:200]}…

" msg = f"""
🔍 Search: {escape(query)} {items}
""" 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"• {title}" if img.get("width") and img.get("height"): items += f" ({img['width']}×{img['height']})" items += "
" search_url = f"https://duckduckgo.com/?q={escape(query)}&iax=images&ia=images" items += f"
🔍 View all images" msg = f"""
🖼️ Images: {escape(query)} {items}
""" 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"• {title}
{body[:200]}…

" msg = f"""
📰 News: {escape(query)} {items}
""" 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"• {title}
" search_url = f"https://duckduckgo.com/?q={escape(query)}&iar=videos" items += f"
🔍 View all videos" msg = f"""
🎬 Videos: {escape(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"🔗 Search with {escape(bang_query)} on DuckDuckGo" msg = f"""
🎯 Bang: {escape(bang_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 = """ 🎯 DuckDuckGo Bangs
Usage: !ddg bang !bang query

Popular bangs:
!w – Wikipedia • !g – Google • !yt – YouTube • !aw – ArchWiki • !gh – GitHub • !so – Stack Overflow • !reddit – Reddit
Full list here """ await bot.api.send_markdown_message(room.room_id, msg) async def send_help(room, bot): help_msg = """ 🦆 DuckDuckGo Commands
!ddg <query> – Top result (collapsible)
!ddg search <query> – 5 web results
!ddg image <query> – 3 images
!ddg news <query> – 3 news articles
!ddg video <query> – 3 videos
!ddg bang <!bang query> – Bang redirect
!ddg define <word> – Definition
!ddg calc <expr> – Calculator
!ddg weather [city] – Weather
!ddg help – 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__ = """
!ddg – DuckDuckGo search (web, images, news, etc.)
  • !ddg <query> – Top web result snippet (collapsible)
  • !ddg search <query> – 5 web results
  • !ddg image <query> – 3 images
  • !ddg news <query> – 3 news articles
  • !ddg video <query> – 3 videos
  • !ddg bang <!bang query> – Bang redirect
  • !ddg define <word> – Definition
  • !ddg calc <expression> – Calculator
  • !ddg weather [location] – Weather

Uses ddgs library. No API key required.

"""