mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-31 18:12:45 +00:00
The two commands carried near-identical copies of fetch-until-short, split-at-a-natural-break and two-part send. modules/joke_text.py now holds them as plain functions (a mixin module under modules/commands would be loaded as a plugin and recorded as a failure). Each command keeps its public method names, its emoji, its own split points (joke also splits on newlines) and its own pause: joke still waits 2 s between the two parts, dadjoke does not. Lengths stay counted in characters against 130, as before.
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) -> 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!"
|