Add comprehensive "soaking" easter egg feature that detects mentions and responds with GIFs showing escalating disappointment based on recency (reversed from legacy - more recent = more disappointed). Features: - Detects "soak", "soaking", "soaked", "soaker" (case-insensitive) - 7 disappointment tiers with 5 varied search phrases each - Giphy API integration with Trump filter and fallback handling - JSON-based persistence tracking all mentions with history - /lastsoak command showing detailed information - 25 comprehensive unit tests (all passing) Architecture: - commands/soak/giphy_service.py - Tiered GIF fetching - commands/soak/tracker.py - JSON persistence with history - commands/soak/listener.py - Message detection and response - commands/soak/info.py - /lastsoak info command - tests/test_commands_soak.py - Full test coverage Uses existing Giphy API key from legacy implementation. Zero new dependencies, follows established patterns. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
196 lines
5.6 KiB
Python
196 lines
5.6 KiB
Python
"""
|
|
Giphy Service for Soak Easter Egg
|
|
|
|
Provides async interface to Giphy API with disappointment-based search phrases.
|
|
"""
|
|
import random
|
|
import logging
|
|
from typing import List, Optional
|
|
import aiohttp
|
|
|
|
logger = logging.getLogger(f'{__name__}.GiphyService')
|
|
|
|
# Giphy API configuration
|
|
GIPHY_API_KEY = 'H86xibttEuUcslgmMM6uu74IgLEZ7UOD'
|
|
GIPHY_TRANSLATE_URL = 'https://api.giphy.com/v1/gifs/translate'
|
|
|
|
# Disappointment tier configuration
|
|
DISAPPOINTMENT_TIERS = {
|
|
'tier_1': {
|
|
'max_seconds': 1800, # 30 minutes
|
|
'phrases': [
|
|
"extremely disappointed",
|
|
"so disappointed",
|
|
"are you kidding me",
|
|
"seriously",
|
|
"unbelievable"
|
|
],
|
|
'description': "Maximum Disappointment"
|
|
},
|
|
'tier_2': {
|
|
'max_seconds': 7200, # 2 hours
|
|
'phrases': [
|
|
"very disappointed",
|
|
"can't believe you",
|
|
"not happy",
|
|
"shame on you",
|
|
"facepalm"
|
|
],
|
|
'description': "Severe Disappointment"
|
|
},
|
|
'tier_3': {
|
|
'max_seconds': 21600, # 6 hours
|
|
'phrases': [
|
|
"disappointed",
|
|
"not impressed",
|
|
"shaking head",
|
|
"eye roll",
|
|
"really"
|
|
],
|
|
'description': "Strong Disappointment"
|
|
},
|
|
'tier_4': {
|
|
'max_seconds': 86400, # 24 hours
|
|
'phrases': [
|
|
"mildly disappointed",
|
|
"not great",
|
|
"could be better",
|
|
"sigh",
|
|
"seriously"
|
|
],
|
|
'description': "Moderate Disappointment"
|
|
},
|
|
'tier_5': {
|
|
'max_seconds': 604800, # 7 days
|
|
'phrases': [
|
|
"slightly disappointed",
|
|
"oh well",
|
|
"shrug",
|
|
"meh",
|
|
"not bad"
|
|
],
|
|
'description': "Mild Disappointment"
|
|
},
|
|
'tier_6': {
|
|
'max_seconds': float('inf'), # 7+ days
|
|
'phrases': [
|
|
"not disappointed",
|
|
"relieved",
|
|
"proud",
|
|
"been worse",
|
|
"fine i guess"
|
|
],
|
|
'description': "Minimal Disappointment"
|
|
},
|
|
'first_ever': {
|
|
'phrases': [
|
|
"here we go",
|
|
"oh boy",
|
|
"uh oh",
|
|
"getting started",
|
|
"and so it begins"
|
|
],
|
|
'description': "The Beginning"
|
|
}
|
|
}
|
|
|
|
|
|
def get_tier_for_seconds(seconds_elapsed: Optional[int]) -> str:
|
|
"""
|
|
Determine disappointment tier based on elapsed time.
|
|
|
|
Args:
|
|
seconds_elapsed: Seconds since last soak, or None for first ever
|
|
|
|
Returns:
|
|
Tier key string (e.g., 'tier_1', 'first_ever')
|
|
"""
|
|
if seconds_elapsed is None:
|
|
return 'first_ever'
|
|
|
|
for tier_key in ['tier_1', 'tier_2', 'tier_3', 'tier_4', 'tier_5', 'tier_6']:
|
|
if seconds_elapsed <= DISAPPOINTMENT_TIERS[tier_key]['max_seconds']:
|
|
return tier_key
|
|
|
|
return 'tier_6' # Fallback to lowest disappointment
|
|
|
|
|
|
def get_random_phrase_for_tier(tier_key: str) -> str:
|
|
"""
|
|
Get a random search phrase from the specified tier.
|
|
|
|
Args:
|
|
tier_key: Tier identifier (e.g., 'tier_1', 'first_ever')
|
|
|
|
Returns:
|
|
Random search phrase from that tier
|
|
"""
|
|
phrases = DISAPPOINTMENT_TIERS[tier_key]['phrases']
|
|
return random.choice(phrases)
|
|
|
|
|
|
def get_tier_description(tier_key: str) -> str:
|
|
"""
|
|
Get the human-readable description for a tier.
|
|
|
|
Args:
|
|
tier_key: Tier identifier
|
|
|
|
Returns:
|
|
Description string
|
|
"""
|
|
return DISAPPOINTMENT_TIERS[tier_key]['description']
|
|
|
|
|
|
async def get_disappointment_gif(tier_key: str) -> Optional[str]:
|
|
"""
|
|
Fetch a GIF from Giphy based on disappointment tier.
|
|
|
|
Randomly selects a search phrase from the tier and queries Giphy.
|
|
Filters out Trump GIFs (legacy behavior).
|
|
Falls back to trying other phrases if first fails.
|
|
|
|
Args:
|
|
tier_key: Tier identifier (e.g., 'tier_1', 'first_ever')
|
|
|
|
Returns:
|
|
GIF URL string, or None if all attempts fail
|
|
"""
|
|
phrases = DISAPPOINTMENT_TIERS[tier_key]['phrases']
|
|
|
|
# Shuffle phrases for variety and retry capability
|
|
shuffled_phrases = random.sample(phrases, len(phrases))
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
for phrase in shuffled_phrases:
|
|
try:
|
|
url = f"{GIPHY_TRANSLATE_URL}?s={phrase}&api_key={GIPHY_API_KEY}"
|
|
|
|
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp:
|
|
if resp.status == 200:
|
|
data = await resp.json()
|
|
|
|
# Filter out Trump GIFs (legacy behavior)
|
|
gif_title = data.get('data', {}).get('title', '').lower()
|
|
if 'trump' in gif_title:
|
|
logger.debug(f"Filtered out Trump GIF for phrase: {phrase}")
|
|
continue
|
|
|
|
gif_url = data.get('data', {}).get('url')
|
|
if gif_url:
|
|
logger.info(f"Successfully fetched GIF for phrase: {phrase}")
|
|
return gif_url
|
|
else:
|
|
logger.warning(f"No GIF URL in response for phrase: {phrase}")
|
|
else:
|
|
logger.warning(f"Giphy API returned status {resp.status} for phrase: {phrase}")
|
|
|
|
except aiohttp.ClientError as e:
|
|
logger.error(f"HTTP error fetching GIF for phrase '{phrase}': {e}")
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error fetching GIF for phrase '{phrase}': {e}")
|
|
|
|
# All phrases failed
|
|
logger.error(f"Failed to fetch any GIF for tier: {tier_key}")
|
|
return None
|