Files
derp/plugins/searx.py
user f046cced28 fix: use public SearXNG URL without proxy
Switch to searx.mymx.me (public domain) for SearXNG queries
without routing through SOCKS5 proxy. Internal address is not
reachable from the container network.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 18:28:29 +01:00

95 lines
2.6 KiB
Python

"""Plugin: SearXNG web search."""
from __future__ import annotations
import json
import urllib.parse
import urllib.request
from derp.plugin import command
# -- Constants ---------------------------------------------------------------
_SEARX_URL = "https://searx.mymx.me/search"
_FETCH_TIMEOUT = 10
_MAX_RESULTS = 3
_MAX_TITLE_LEN = 80
_MAX_QUERY_LEN = 200
# -- Pure helpers ------------------------------------------------------------
def _truncate(text: str, max_len: int = _MAX_TITLE_LEN) -> str:
"""Truncate text with ellipsis if needed."""
if len(text) <= max_len:
return text
return text[: max_len - 3].rstrip() + "..."
# -- SearXNG search (blocking) ----------------------------------------------
def _search(query: str) -> list[dict]:
"""Search SearXNG. Blocking.
Returns list of dicts with keys: title, url, snippet.
Raises on HTTP or parse errors.
"""
params = urllib.parse.urlencode({"q": query, "format": "json"})
url = f"{_SEARX_URL}?{params}"
req = urllib.request.Request(url, method="GET")
resp = urllib.request.urlopen(req, timeout=_FETCH_TIMEOUT)
raw = resp.read()
resp.close()
data = json.loads(raw)
results: list[dict] = []
for item in data.get("results", []):
results.append({
"title": item.get("title", ""),
"url": item.get("url", ""),
"snippet": item.get("content", item.get("snippet", "")),
})
return results
# -- Command handler ---------------------------------------------------------
@command("searx", help="Search: !searx <query>")
async def cmd_searx(bot, message):
"""Search SearXNG and show top results.
Usage: !searx <query...>
"""
if not message.is_channel:
await bot.reply(message, "Use this command in a channel")
return
parts = message.text.split(None, 1)
if len(parts) < 2 or not parts[1].strip():
await bot.reply(message, "Usage: !searx <query>")
return
query = parts[1].strip()
if len(query) > _MAX_QUERY_LEN:
await bot.reply(message, f"Query too long (max {_MAX_QUERY_LEN} chars)")
return
import asyncio
loop = asyncio.get_running_loop()
try:
results = await loop.run_in_executor(None, _search, query)
except Exception as exc:
await bot.reply(message, f"Search failed: {exc}")
return
if not results:
await bot.reply(message, f"No results for: {query}")
return
for item in results[:_MAX_RESULTS]:
title = _truncate(item["title"]) if item["title"] else "(no title)"
url = item["url"]
await bot.reply(message, f"{title} -- {url}")