quote plugin added. Fixed funguy.py and plugins.py for plugin list.
This commit is contained in:
@@ -213,46 +213,6 @@ class FunguyBot:
|
|||||||
await self.bot.api.send_text_message(room.room_id, "You are not authorized to enable plugins.") # Sending unauthorized message
|
await self.bot.api.send_text_message(room.room_id, "You are not authorized to enable plugins.") # Sending unauthorized message
|
||||||
return
|
return
|
||||||
|
|
||||||
# List plugins command with descriptions (bold plugin names)
|
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("plugins"):
|
|
||||||
if self.PLUGINS:
|
|
||||||
# Build a list with plugin names and their descriptions
|
|
||||||
plugin_list = []
|
|
||||||
for plugin_name in sorted(self.PLUGINS.keys()):
|
|
||||||
plugin_module = self.PLUGINS[plugin_name]
|
|
||||||
# Try to get description from plugin
|
|
||||||
description = getattr(plugin_module, "__description__", None)
|
|
||||||
if not description:
|
|
||||||
# Try to get from docstring
|
|
||||||
docstring = getattr(plugin_module, "__doc__", None)
|
|
||||||
if docstring:
|
|
||||||
# Get first line of docstring
|
|
||||||
description = docstring.strip().split('\n')[0]
|
|
||||||
else:
|
|
||||||
description = "No description available"
|
|
||||||
|
|
||||||
# Make plugin name bold using HTML <strong> tag
|
|
||||||
plugin_list.append(f"<strong>[{plugin_name}.py]</strong>: {description}")
|
|
||||||
|
|
||||||
# Send as HTML message (bold will render)
|
|
||||||
response = "\n".join(plugin_list)
|
|
||||||
# Split into multiple messages if too long
|
|
||||||
if len(response) > 4000:
|
|
||||||
chunk = ""
|
|
||||||
for line in plugin_list:
|
|
||||||
if len(chunk) + len(line) + 1 > 4000:
|
|
||||||
await self.bot.api.send_markdown_message(room.room_id, chunk)
|
|
||||||
chunk = line + "\n"
|
|
||||||
else:
|
|
||||||
chunk += line + "\n"
|
|
||||||
if chunk:
|
|
||||||
await self.bot.api.send_markdown_message(room.room_id, chunk)
|
|
||||||
else:
|
|
||||||
await self.bot.api.send_markdown_message(room.room_id, response)
|
|
||||||
else:
|
|
||||||
await self.bot.api.send_text_message(room.room_id, "No plugins are currently loaded.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Rehash config command
|
# Rehash config command
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("rehash"):
|
if match.is_not_from_this_bot() and match.prefix() and match.command("rehash"):
|
||||||
if str(message.sender) == self.config.admin_user: # Checking if sender is admin user
|
if str(message.sender) == self.config.admin_user: # Checking if sender is admin user
|
||||||
|
|||||||
+33
-31
@@ -1,11 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
This plugin provides a command to list all loaded plugins along with their descriptions.
|
This plugin lists all loaded plugins (from bot.plugins) inside a collapsible block,
|
||||||
|
and sends a separate always‑visible line showing the total active count.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# plugins/plugins.py
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import logging
|
import logging
|
||||||
import simplematrixbotlib as botlib
|
import simplematrixbotlib as botlib
|
||||||
|
|
||||||
@@ -13,46 +10,51 @@ async def handle_command(room, message, bot, prefix, config):
|
|||||||
match = botlib.MessageMatch(room, message, bot, prefix)
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("plugins"):
|
if match.is_not_from_this_bot() and match.prefix() and match.command("plugins"):
|
||||||
logging.info("Received !plugins command")
|
logging.info("Received !plugins command")
|
||||||
plugin_descriptions = get_plugin_descriptions()
|
|
||||||
|
|
||||||
plugin_descriptions.insert(0, "<details><summary><strong>🔌Plugins List🔌<br>⤵︎Click Here to Expand⤵︎</strong></summary>")
|
# Fetch the real, currently loaded plugins from the bot instance
|
||||||
plugins_message = "<br>".join(plugin_descriptions)
|
plugins_dict = getattr(bot, "plugins", {})
|
||||||
plugins_message += "</details>"
|
if not plugins_dict:
|
||||||
|
await bot.api.send_markdown_message(room.room_id, "📊 **Total Active Plugins:** 0")
|
||||||
|
return
|
||||||
|
|
||||||
await bot.api.send_markdown_message(room.room_id, plugins_message)
|
# Build sorted list of descriptions
|
||||||
logging.info("Sent plugin list to the room")
|
plugin_items = []
|
||||||
|
for plugin_name, module in sorted(plugins_dict.items()):
|
||||||
|
desc = getattr(module, "__description__", None)
|
||||||
|
if not desc and module.__doc__:
|
||||||
|
desc = module.__doc__.strip().split("\n")[0]
|
||||||
|
desc = desc or "No description available"
|
||||||
|
plugin_items.append(f"<strong>[{plugin_name}.py]</strong>: {desc}")
|
||||||
|
|
||||||
|
active_count = len(plugin_items)
|
||||||
|
|
||||||
def get_plugin_descriptions():
|
# 1) Always‑visible count
|
||||||
plugin_descriptions = []
|
count_msg = f"📊 **Total Active Plugins:** {active_count}"
|
||||||
for module_name, module in sys.modules.items():
|
await bot.api.send_markdown_message(room.room_id, count_msg)
|
||||||
if module_name.startswith("plugins.") and hasattr(module, "__file__"):
|
|
||||||
plugin_path = module.__file__
|
|
||||||
plugin_name = os.path.basename(plugin_path).split(".")[0]
|
|
||||||
|
|
||||||
if hasattr(module, "__description__"):
|
# 2) Collapsible list
|
||||||
description = module.__description__
|
list_html = "<br>".join(plugin_items)
|
||||||
elif module.__doc__:
|
detail_msg = (
|
||||||
description = module.__doc__.strip().split("\n")[0]
|
f"<details>"
|
||||||
else:
|
f"<summary><strong>🔌 Plugin List 🔌<br>⤵︎ Click to expand</strong></summary>"
|
||||||
description = "No description available"
|
f"<br>{list_html}"
|
||||||
|
f"</details>"
|
||||||
|
)
|
||||||
|
await bot.api.send_markdown_message(room.room_id, detail_msg)
|
||||||
|
|
||||||
plugin_descriptions.append(f"<strong>[{plugin_name}.py]:</strong> {description}")
|
logging.info(f"Sent plugin list ({active_count} active) to room {room.room_id}")
|
||||||
|
|
||||||
plugin_descriptions.sort()
|
|
||||||
return plugin_descriptions
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Plugin Metadata
|
# Plugin Metadata
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
__version__ = "1.0.4"
|
||||||
__author__ = "Funguy Bot"
|
__author__ = "Funguy Bot"
|
||||||
__description__ = "List all loaded plugins"
|
__description__ = "List all loaded plugins with count, collapsible"
|
||||||
__help__ = """
|
__help__ = """
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>!plugins</strong> – List loaded plugins</summary>
|
<summary><strong>!plugins</strong> – List active plugins</summary>
|
||||||
<p>Displays all currently loaded plugins and their descriptions.</p>
|
<p>Shows the total number of loaded plugins and a collapsible list with descriptions.</p>
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
"""
|
||||||
|
Goodreads Quote Scraper – Playwright (headless Chromium)
|
||||||
|
No external APIs, no keys; scrapes directly from goodreads.com
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import asyncio
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
logger = logging.getLogger("quote")
|
||||||
|
|
||||||
|
GR_POPULAR = "https://www.goodreads.com/quotes"
|
||||||
|
GR_SEARCH = "https://www.goodreads.com/quotes/search"
|
||||||
|
QUOTES_PER_PAGE = 30
|
||||||
|
MAX_SEARCH_PAGES = 3
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Playwright browser (shared, launched once)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
_browser = None
|
||||||
|
_playwright = None
|
||||||
|
|
||||||
|
async def _get_browser():
|
||||||
|
global _browser, _playwright
|
||||||
|
if _browser is None:
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
_playwright = await async_playwright().start()
|
||||||
|
_browser = await _playwright.chromium.launch(headless=True)
|
||||||
|
logger.info("Playwright browser started")
|
||||||
|
return _browser
|
||||||
|
|
||||||
|
async def _close_browser():
|
||||||
|
global _browser, _playwright
|
||||||
|
if _browser:
|
||||||
|
await _browser.close()
|
||||||
|
_browser = None
|
||||||
|
if _playwright:
|
||||||
|
await _playwright.stop()
|
||||||
|
_playwright = None
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# HTML parsing (Goodreads specific)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _extract_quotes(html: str) -> list[dict]:
|
||||||
|
"""Parse Goodreads HTML and return a list of {content, author} dicts."""
|
||||||
|
soup = BeautifulSoup(html, "lxml")
|
||||||
|
quotes = []
|
||||||
|
|
||||||
|
for div in soup.find_all("div", class_="quoteText"):
|
||||||
|
full_text = div.get_text(" ", strip=True)
|
||||||
|
# Try curly quotes
|
||||||
|
m = re.search(r"“(.+?)”", full_text)
|
||||||
|
if not m:
|
||||||
|
m = re.search(r"(.+?)\s*―", full_text)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
content = m.group(1).strip()
|
||||||
|
|
||||||
|
author_span = div.find("span", class_="authorOrTitle")
|
||||||
|
author = author_span.get_text(strip=True).rstrip(",") if author_span else "Unknown"
|
||||||
|
quotes.append({"content": content, "author": author})
|
||||||
|
|
||||||
|
# Alternative layout (if first method yielded nothing)
|
||||||
|
for div in soup.find_all("div", class_="quoteDetails"):
|
||||||
|
text_elem = div.find("div", class_="quoteText")
|
||||||
|
author_elem = div.find("span", class_="authorOrTitle")
|
||||||
|
if text_elem:
|
||||||
|
content = text_elem.get_text(strip=True).strip("“”")
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
author = author_elem.get_text(strip=True).rstrip(",") if author_elem else "Unknown"
|
||||||
|
quotes.append({"content": content, "author": author})
|
||||||
|
|
||||||
|
return quotes
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Page fetching
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
async def _scrape(url: str, params: dict = None) -> str:
|
||||||
|
browser = await _get_browser()
|
||||||
|
context = await browser.new_context(
|
||||||
|
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
|
||||||
|
)
|
||||||
|
page = await context.new_page()
|
||||||
|
try:
|
||||||
|
if params:
|
||||||
|
full_url = f"{url}?{urlencode(params)}"
|
||||||
|
else:
|
||||||
|
full_url = url
|
||||||
|
await page.goto(full_url, wait_until="networkidle", timeout=15000)
|
||||||
|
html = await page.content()
|
||||||
|
return html
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load {full_url}: {e}")
|
||||||
|
return ""
|
||||||
|
finally:
|
||||||
|
await page.close()
|
||||||
|
await context.close()
|
||||||
|
|
||||||
|
async def get_random_popular() -> list[dict]:
|
||||||
|
html = await _scrape(GR_POPULAR)
|
||||||
|
return _extract_quotes(html)
|
||||||
|
|
||||||
|
async def get_author_quotes(author: str) -> list[dict]:
|
||||||
|
all_quotes = []
|
||||||
|
for page in range(1, MAX_SEARCH_PAGES + 1):
|
||||||
|
html = await _scrape(GR_SEARCH, {"q": author, "commit": "Search", "page": page})
|
||||||
|
page_quotes = _extract_quotes(html)
|
||||||
|
all_quotes.extend(page_quotes)
|
||||||
|
if len(page_quotes) < QUOTES_PER_PAGE:
|
||||||
|
break
|
||||||
|
return all_quotes
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Formatting
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def format_quote(q: dict) -> str:
|
||||||
|
return f'"{q["content"]}"\n\n— {q["author"]}'
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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("quote")):
|
||||||
|
return
|
||||||
|
|
||||||
|
args = match.args()
|
||||||
|
|
||||||
|
# Help
|
||||||
|
if args and args[0].lower() in ("help", "-h", "--help"):
|
||||||
|
help_html = (
|
||||||
|
"<details><summary><strong>📖 !quote help</strong></summary>"
|
||||||
|
"<ul>"
|
||||||
|
"<li><code>!quote</code> – random popular quote from Goodreads</li>"
|
||||||
|
"<li><code>!quote <author></code> – random quote by that author</li>"
|
||||||
|
"<li><code>!quote help</code> – this</li>"
|
||||||
|
"</ul>"
|
||||||
|
"<p><b>Examples:</b><br><code>!quote</code><br>"
|
||||||
|
"<code>!quote Terence McKenna</code><br>"
|
||||||
|
"<code>!quote Oscar Wilde</code></p>"
|
||||||
|
"<p>Scraped with Playwright (headless browser).</p>"
|
||||||
|
"</details>"
|
||||||
|
)
|
||||||
|
await bot.api.send_markdown_message(room.room_id, help_html)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args:
|
||||||
|
author = " ".join(args).strip()
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id, f"🔍 Searching Goodreads for quotes by **{author}**…"
|
||||||
|
)
|
||||||
|
quotes = await get_author_quotes(author)
|
||||||
|
if not quotes:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"❌ No quotes found for '**{author}**'. Try a different spelling."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
chosen = random.choice(quotes)
|
||||||
|
else:
|
||||||
|
await bot.api.send_text_message(room.room_id, "✨ Fetching a random popular quote…")
|
||||||
|
quotes = await get_random_popular()
|
||||||
|
if not quotes:
|
||||||
|
await bot.api.send_text_message(room.room_id, "❌ Could not fetch any quotes.")
|
||||||
|
return
|
||||||
|
chosen = random.choice(quotes)
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, format_quote(chosen))
|
||||||
|
logger.info(f"Quote sent: {chosen['author']}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Unexpected error in quote plugin")
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id, f"❌ Scraping error: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Plugin metadata
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
__version__ = "1.0.1"
|
||||||
|
__author__ = "Funguy Bot"
|
||||||
|
__description__ = "Goodreads quotes via Playwright (headless browser)"
|
||||||
|
__help__ = """
|
||||||
|
<details>
|
||||||
|
<summary><strong>!quote</strong> – Quotes from Goodreads (scraped with Playwright)</summary>
|
||||||
|
<ul>
|
||||||
|
<li><code>!quote</code> – random popular quote</li>
|
||||||
|
<li><code>!quote <author></code> – random quote by that author</li>
|
||||||
|
<li><code>!quote help</code></li>
|
||||||
|
</ul>
|
||||||
|
<p>No API keys, no JSON files – just a real browser fetching from Goodreads.</p>
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
@@ -21,3 +21,6 @@ omdbapi
|
|||||||
apscheduler
|
apscheduler
|
||||||
pytz
|
pytz
|
||||||
ddgs
|
ddgs
|
||||||
|
playwright
|
||||||
|
lxml
|
||||||
|
beautifulsoup4
|
||||||
Reference in New Issue
Block a user