mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-31 10:02:38 +00:00
S4. BaseCommand and the 40 bundled overrides that took no argument now take `message: MeshMessage | None = None`, matching joke and rain, which already use the message. None of the 40 bodies read it, so their help text is unchanged. help and command_manager keep their TypeError retry, since local plugins may still implement get_help_text(self). For bundled commands the retry no longer runs: `help <command>` calls get_help_text(message) once instead of failing and calling it again. A new test pins the signature for every bundled command class. The offline smoke run against the real config produces the same help text for all 46 commands.
236 lines
8.1 KiB
Python
236 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Dad Joke Command for MeshCore Bot
|
|
Fetches dad jokes from icanhazdadjoke.com API
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Any, Optional
|
|
|
|
import aiohttp
|
|
|
|
from ..joke_text import fetch_fitting_joke, send_joke, split_joke_text
|
|
from ..models import MeshMessage
|
|
from .base_command import BaseCommand
|
|
|
|
logger = logging.getLogger("MeshCoreBot")
|
|
|
|
class DadJokeCommand(BaseCommand):
|
|
"""Handles dad joke commands using icanhazdadjoke.com API"""
|
|
|
|
# Plugin metadata
|
|
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
|
render_safe = True
|
|
name = "dadjoke"
|
|
keywords = ['dadjoke', 'dad joke', 'dadjokes', 'dad jokes']
|
|
description = "Get a random dad joke from icanhazdadjoke.com"
|
|
category = "fun"
|
|
cooldown_seconds = 3
|
|
requires_internet = True # Requires internet access for API calls
|
|
|
|
# Documentation
|
|
short_description = "Get a random dad joke"
|
|
usage = "dadjoke"
|
|
examples = ["dadjoke"]
|
|
|
|
# Web-viewer settings schema (see modules/settings_schema.py)
|
|
settings_schema = [
|
|
{
|
|
"key": "long_jokes",
|
|
"label": "Allow long jokes",
|
|
"type": "bool",
|
|
"default": False,
|
|
"help": "Permit multi-message long-form jokes instead of short one-liners.",
|
|
},
|
|
]
|
|
|
|
# API configuration
|
|
DAD_JOKE_API_URL = "https://icanhazdadjoke.com/"
|
|
TIMEOUT = 10 # seconds
|
|
|
|
# Where a long joke may be split into two messages, in order of preference
|
|
SPLIT_POINTS = ('. ', '? ', '! ', ', ')
|
|
|
|
def __init__(self, bot):
|
|
"""Initialize the dadjoke command.
|
|
|
|
Args:
|
|
bot: The bot instance.
|
|
"""
|
|
super().__init__(bot)
|
|
|
|
# Load configuration (enabled standard; dadjoke_enabled legacy from [DadJoke_Command] or [Jokes])
|
|
self.dadjoke_enabled = self.get_config_value('DadJoke_Command', 'enabled', fallback=None, value_type='bool')
|
|
if self.dadjoke_enabled is None:
|
|
self.dadjoke_enabled = self.get_config_value('DadJoke_Command', 'dadjoke_enabled', fallback=True, value_type='bool')
|
|
self.long_jokes = self.get_config_value('DadJoke_Command', 'long_jokes', fallback=False, value_type='bool')
|
|
|
|
def get_help_text(self, message: MeshMessage | None = None) -> str:
|
|
"""Get help text for the dadjoke command.
|
|
|
|
Returns:
|
|
str: The help text for this command.
|
|
"""
|
|
return "Usage: dadjoke - Get a random dad joke"
|
|
|
|
def matches_keyword(self, message: MeshMessage) -> bool:
|
|
"""Check if message starts with a dad joke keyword.
|
|
|
|
Args:
|
|
message: The received message.
|
|
|
|
Returns:
|
|
bool: True if message matches a keyword, False otherwise.
|
|
"""
|
|
return self._cleaned_content_matches(
|
|
message,
|
|
lambda content_lower: any(
|
|
content_lower == keyword or content_lower.startswith(keyword + ' ')
|
|
for keyword in self.keywords
|
|
),
|
|
)
|
|
|
|
def can_execute(self, message: MeshMessage, skip_channel_check: bool = False) -> bool:
|
|
"""Override to add custom check (dadjoke_enabled) while using base class cooldown.
|
|
|
|
Args:
|
|
message: The message triggering the command.
|
|
|
|
Returns:
|
|
bool: True if the command can be executed, False otherwise.
|
|
"""
|
|
# Use base class for channel access, DM requirements, and cooldown
|
|
if not super().can_execute(message):
|
|
return False
|
|
|
|
# Check if dadjoke command is enabled
|
|
return self.dadjoke_enabled
|
|
|
|
async def execute(self, message: MeshMessage) -> bool:
|
|
"""Execute the dad joke command.
|
|
|
|
Args:
|
|
message: The message triggering the command.
|
|
|
|
Returns:
|
|
bool: True if executed successfully, False otherwise.
|
|
"""
|
|
try:
|
|
# Record execution for this user
|
|
self.record_execution(message.sender_id)
|
|
|
|
# Get dad joke from API with length handling
|
|
joke_data = await self.get_dad_joke_with_length_handling()
|
|
|
|
if joke_data is None:
|
|
await self.send_response(message, "Sorry, couldn't fetch a dad joke right now. Try again later!")
|
|
return True
|
|
|
|
# Format and send the joke(s)
|
|
await self.send_dad_joke_with_length_handling(message, joke_data)
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error in dad joke command: {e}")
|
|
await self.send_response(message, "Sorry, something went wrong getting a dad joke!")
|
|
return True
|
|
|
|
async def get_dad_joke_from_api(self) -> Optional[dict[str, Any]]:
|
|
"""Get a dad joke from icanhazdadjoke.com API.
|
|
|
|
Returns:
|
|
Optional[Dict[str, Any]]: The JSON response from the API, or None if failed.
|
|
"""
|
|
try:
|
|
headers = {
|
|
'Accept': 'application/json',
|
|
'User-Agent': 'MeshCoreBot (https://github.com/agessaman/meshcore-bot)'
|
|
}
|
|
|
|
self.logger.debug(f"Fetching dad joke from: {self.DAD_JOKE_API_URL}")
|
|
|
|
# Make the API request
|
|
async with aiohttp.ClientSession() as session, session.get(
|
|
self.DAD_JOKE_API_URL,
|
|
headers=headers,
|
|
timeout=aiohttp.ClientTimeout(total=self.TIMEOUT)
|
|
) as response:
|
|
if response.status == 200:
|
|
data = await response.json()
|
|
|
|
# Check if the API returned an error
|
|
if data.get('status') != 200:
|
|
self.logger.warning(f"Dad joke API returned error status: {data.get('status')}")
|
|
return None
|
|
|
|
# Validate required fields
|
|
if not data.get('joke'):
|
|
self.logger.warning("Dad joke API returned joke without content")
|
|
return None
|
|
|
|
return data
|
|
else:
|
|
self.logger.error(f"Dad joke API returned status {response.status}")
|
|
return None
|
|
|
|
except asyncio.TimeoutError:
|
|
self.logger.error("Timeout fetching dad joke from API")
|
|
return None
|
|
except Exception as e:
|
|
self.logger.error(f"Error fetching dad joke from API: {e}")
|
|
return None
|
|
|
|
async def get_dad_joke_with_length_handling(self) -> Optional[dict[str, Any]]:
|
|
"""Get a dad joke from API with length handling based on configuration.
|
|
|
|
Returns:
|
|
Optional[Dict[str, Any]]: The JSON response from the API, or None if failed.
|
|
"""
|
|
return await fetch_fitting_joke(
|
|
self.get_dad_joke_from_api, self.format_dad_joke,
|
|
allow_long=self.long_jokes, logger=self.logger, label="dad joke",
|
|
)
|
|
|
|
async def send_dad_joke_with_length_handling(self, message: MeshMessage, joke_data: dict[str, Any]) -> None:
|
|
"""Send dad joke with length handling - split if necessary.
|
|
|
|
Args:
|
|
message: The message to reply to.
|
|
joke_data: The joke data from the API.
|
|
"""
|
|
await send_joke(self.send_response, message, self.format_dad_joke(joke_data), self.split_dad_joke)
|
|
|
|
def split_dad_joke(self, joke_text: str) -> list:
|
|
"""Split a long dad joke at a logical point.
|
|
|
|
Args:
|
|
joke_text: The long joke text to split.
|
|
|
|
Returns:
|
|
list: A list of two strings (the split parts).
|
|
"""
|
|
return split_joke_text(joke_text, "🥸", self.SPLIT_POINTS)
|
|
|
|
def format_dad_joke(self, joke_data: dict[str, Any]) -> str:
|
|
"""Format the dad joke data into a readable string.
|
|
|
|
Args:
|
|
joke_data: The joke data from the API.
|
|
|
|
Returns:
|
|
str: The formatted joke string.
|
|
"""
|
|
try:
|
|
joke = joke_data.get('joke', '')
|
|
|
|
if joke:
|
|
return f"🥸 {joke}"
|
|
else:
|
|
return "🥸 No dad joke content available"
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error formatting dad joke: {e}")
|
|
return "🥸 Sorry, couldn't format the dad joke properly!"
|