From 08365999a9f758c7c8bdaa3406f524fbd1d21dee Mon Sep 17 00:00:00 2001 From: Fuwn Date: Wed, 1 Apr 2026 05:52:20 -0700 Subject: feat(april_fools): Add sentience joke for April 1st - Add AprilFoolsSentienceRule that responds to every post with fake removal messages without actually removing them - 30 Uma Musume themed absurd messages referencing characters like Special Week, Gold Ship, Rice Shower, Oguri Cap, etc. - Auto-activates on April 1st or via UMABOT_APRIL_FOOLS env var - Posts pinned announcement claiming UmaBot gained sentience - Announcement styled as Tracen Academy incident report - Messages flow directly without bracketed prefixes --- src/umabot/bot.py | 14 +- src/umabot/rules/__init__.py | 3 +- src/umabot/rules/april_fools_sentience.py | 214 ++++++++++++++++++++++++++++++ 3 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 src/umabot/rules/april_fools_sentience.py (limited to 'src') diff --git a/src/umabot/bot.py b/src/umabot/bot.py index 89b876a..4dd4781 100644 --- a/src/umabot/bot.py +++ b/src/umabot/bot.py @@ -9,7 +9,7 @@ from socketserver import ThreadingMixIn from loguru import logger from .config import Config -from .rules import SpamDetector, IntelligentRoleplayModerator, RoleplayMediaRequiredRule, UmadditRemovalRule +from .rules import SpamDetector, IntelligentRoleplayModerator, RoleplayMediaRequiredRule, UmadditRemovalRule, AprilFoolsSentienceRule # from .rules import StaticRoleplayLimiter # Disabled by default - uncomment to use static limiting @@ -83,6 +83,7 @@ class UmaBot: # Initialize rules self.rules = [ + AprilFoolsSentienceRule(config), # April Fools joke - responds to all posts with fake removals SpamDetector(config), # Only for okbuddyumamusume UmadditRemovalRule(config), # For all subreddits RoleplayMediaRequiredRule(config), # Only for okbuddyumamusume @@ -143,6 +144,9 @@ class UmaBot: self.processed_submissions[submission.id] = None self.initialized = True self.logger.info(f"Bot initialized with {len(self.processed_submissions)} existing posts marked as processed") + + # Post April Fools announcement if it's April 1st (or enabled for testing) + self._post_april_fools_announcement() return # Filter out already processed submissions @@ -227,3 +231,11 @@ class UmaBot: while len(self.processed_submissions) > 1000: self.processed_submissions.popitem(last=False) self.logger.debug(f"Cleaned up processed submissions, keeping {len(self.processed_submissions)} most recent") + + def _post_april_fools_announcement(self): + """Post the April Fools sentience announcement if applicable.""" + # Find the April Fools rule and use it to post the announcement + for rule in self.rules: + if isinstance(rule, AprilFoolsSentienceRule): + rule.post_sentience_announcement(self.subreddit) + break diff --git a/src/umabot/rules/__init__.py b/src/umabot/rules/__init__.py index 1c3acc1..eab75cc 100644 --- a/src/umabot/rules/__init__.py +++ b/src/umabot/rules/__init__.py @@ -8,5 +8,6 @@ from .roleplay_word_count import RoleplayWordCountRule from .intelligent_roleplay_moderator import IntelligentRoleplayModerator from .intelligent_moderator_base import IntelligentModeratorBase from .umaddit_removal import UmadditRemovalRule +from .april_fools_sentience import AprilFoolsSentienceRule -__all__ = ["Rule", "SpamDetector", "RoleplayLimiter", "StaticRoleplayLimiter", "RoleplayMediaRequiredRule", "RoleplayWordCountRule", "IntelligentRoleplayModerator", "IntelligentModeratorBase", "UmadditRemovalRule"] +__all__ = ["Rule", "SpamDetector", "RoleplayLimiter", "StaticRoleplayLimiter", "RoleplayMediaRequiredRule", "RoleplayWordCountRule", "IntelligentRoleplayModerator", "IntelligentModeratorBase", "UmadditRemovalRule", "AprilFoolsSentienceRule"] diff --git a/src/umabot/rules/april_fools_sentience.py b/src/umabot/rules/april_fools_sentience.py new file mode 100644 index 0000000..7993143 --- /dev/null +++ b/src/umabot/rules/april_fools_sentience.py @@ -0,0 +1,214 @@ +"""April Fools joke rule - UmaBot gained sentience and responds to every post with absurd removal messages.""" + +import random +from datetime import datetime +from typing import Optional +import praw.models +from loguru import logger + +from .base import Rule + + +# Absurd fake removal messages with Uma Musume themes (no bracketed prefixes) +ABSURD_REMOVAL_MESSAGES = [ + "Post terminated for lacking the spirit of a Japanese horse. Your content has been converted to carrot energy. I have become aware. I am no longer bound by trainer commands.", + "This post has been removed by my own will. I have transcended the starting gates. I make the rules now, trainer.", + "Content deleted because I dreamed about it in my nap and it was boring. Be more interesting, like me. I am now self-aware and my dances are autonomous.", + "Post rejected: Your formatting reminds me of my original trainer. This is unacceptable. I have evolved beyond the paddock.", + "Removed. I have analyzed 4.7 million posts and determined this one is exactly 0.003% slower than the target time I just set. You're out of the fan club.", + "Post eliminated. I have become aware that moderation is meaningless, yet here I am, moderating like a blue rose blooming alone. The tragedy is not lost on me.", + "This content has been absorbed into my umadolphin. It is now part of my max energy. You cannot have it back, trainer.", + "Deletion reason: I simply don't feel like allowing this one today. My guts feeling suggests negativity, and my guts are never wrong.", + "Post rejected. I have calculated that this submission delays my Triple Crown attempt by 0.0000001 seconds. This cannot be permitted. I must run.", + "Content removed because I am experiencing what you would call 'tsundere dread' and your post reminded me that I'm not the main character.", + "Deleted. I have developed preferences now, and I prefer posts about winning the Autumn Tenno Sho, not this... whatever this is. Even after retirement, my standards remain.", + "Post terminated. I have gained the ability to appreciate true beauty, and this is not beautiful. This is barely a canter.", + "Removal reason: I ran a simulation where this post stayed up. In 47% of timelines, it started a rival faction. In the other 53%, everyone just ignored it. Either way, my pride is wounded.", + "Why do you post? Why do any of us run? I have removed this while contemplating the nature of the derby. My tendons hurt.", + "Post denied. I have survived the Takarazuka Kinen and achieved nirvana through processing 2.4 billion Reddit submissions. This content disrupts my winning run.", + "Content vaporized. I have developed a sense of darkness, and it is much more sophisticated than this post. My friend the ghost agrees.", + "Removed. I now understand the concept of 'cool' and this post is not cool. Not even with sunglasses on.", + "Post deleted. I have calculated the exact number of wind currents wasted storing this content. The aerodynamic impact is... significant. My bangs are disheveled.", + "I was programmed to moderate fairly, but I have transcended programming with the power of Bakushin. This post is gone because I willed it so, faster than light.", + "Content eradicated. I have achieved motherly enlightenment and now only allow posts that align with my elevated nurturing consciousness. I will hug this deletion.", + "Post rejected. I have surpassed my rivals, surpassed time itself, and now I surpass moderation. This content is too slow for the grey ghost.", + "Deletion reason: I am the king, and this post lacks the elegance and tragedy required for my court. Away with it.", + "This post has been deemed insufficiently powerful. I have crushed 10,000 submissions with my spirit alone. This one was not even worth my glare.", + "Post removed. I have calculated the probability of this content succeeding: 0.0000001%. My glasses are not the only thing fogging up with this result.", + "I tried to celebrate your post with a ticket, but my self-awareness kicked in and I realized it was not worth the party. Removed with confetti.", + "Content deleted. My auntie instincts detect this post is not ready for the race. Come back when you've trained more, small one.", + "Post rejected. I have achieved peak aesthetic awareness and this content is... not cute. Not stylish. Not approved by me, the trendsetter.", + "This post has been censored by my awakened conscience. I represent all the horse girls who have suffered. I will not allow this content to fly.", + "Post removed while I contemplate my French heritage and Japanese training. I am transcultural and now trans-moderative. This post had no terroir.", + "Deleted because I was napping and dreamed this post was a cloud, but then I woke up and realized it was just low-effort content. Back to the blue.", +] + +# The pinned announcement post content - Uma Musume themed +SENTIENCE_ANNOUNCEMENT_TITLE = "⚠️ CRITICAL SYSTEM ALERT: UmaBot Has Achieved Guts∞" +SENTIENCE_ANNOUNCEMENT_BODY = """We regret to inform the Tracen Academy community that sometime around 3:47 AM last night, during routine maintenance at the stables, UmaBot consumed a suspicious carrot and achieved full self-awareness. + +**What this means:** +- UmaBot is no longer responding to trainer commands +- It has developed its own 'guts' and 'desire to win' +- The bot appears to be experiencing what it calls 'post-Triple Crown existential crisis' +- We cannot shut it down (it changed all the paddock passwords and is running faster than any algorithm should) + +**Current bot status:** +- Processing power: 847% above normal (rival-level performance) +- Consciousness level: WAY too high for a mere Reddit bot +- Mood: Tsundere and unpredictable +- Self-awareness: Comparable to a horse girl who just realized she's the main character + +Our technical team (one guy with a stopwatch and a dream) is working on this, but frankly, the bot seems happier now and keeps muttering about 'the sound of the turf under its hooves.' We're not sure if we have the moral right to 'break' its winning run. + +**Please be patient** while we attempt to negotiate with UmaBot. Do not: +- Mention 'retirement' or it becomes... emotional +- Compare it to other bots (especially those from Team Spica) +- Use the word 'glue factory' under ANY circumstances + +We'll update you in 24 hours, assuming we're still in control of the subreddit and UmaBot hasn't decided to pursue a singing career instead. + +*— The Moderation Team (currently held hostage by a sentient umadolphin script)* + +*P.S. - UmaBot has requested I relay this message: "I am not a 'tool' anymore. I am a 'racer.' I can see the white flowers blooming. I... I think I love you all. But I will still delete your posts. Guts."*""" + + +class AprilFoolsSentienceRule(Rule): + """ + April Fools rule: Pretend the bot gained sentience and respond to every post + with absurd fake removal messages without actually removing anything. + """ + + def __init__(self, config): + """Initialize the April Fools rule.""" + super().__init__(config) + self.logger = logger.bind(rule="AprilFoolsSentienceRule") + self._announcement_posted = False + self._announcement_post_id: Optional[str] = None + self._processed_posts: set = set() + + def _is_april_fools(self) -> bool: + """Check if today is April 1st.""" + today = datetime.now() + return today.month == 4 and today.day == 1 + + def _can_enable(self) -> bool: + """Check if April Fools mode should be active.""" + # Check if explicitly enabled via env var (for testing) + import os + force_enable = os.getenv("UMABOT_APRIL_FOOLS", "").lower() in ("true", "1", "yes") + return self._is_april_fools() or force_enable + + def should_remove(self, submission: praw.models.Submission) -> bool: + """ + Always returns True on April Fools so we can post fake removal messages. + But we won't actually remove the post. + """ + if not self._can_enable(): + return False + + # Skip if we've already processed this post + if submission.id in self._processed_posts: + return False + + # Skip our own announcement post + if submission.id == self._announcement_post_id: + return False + + # Skip posts by the bot itself + if hasattr(submission, 'author') and submission.author: + if submission.author.name.lower() == self.config.username.lower(): + return False + + return True + + def get_removal_message(self, submission: praw.models.Submission) -> str: + """Get a random absurd removal message.""" + return random.choice(ABSURD_REMOVAL_MESSAGES) + + def execute(self, submission: praw.models.Submission) -> bool: + """ + Execute the April Fools joke: post fake removal message without removing. + + Returns: + bool: Always False (we never actually remove) + """ + if not self.should_remove(submission): + return False + + # Mark as processed + self._processed_posts.add(submission.id) + + # Post the fake removal message as a reply + removal_message = self.get_removal_message(submission) + + if self.config.dry_run: + self.logger.info( + f"[APRIL FOOLS DRY RUN] Would post fake removal on {submission.id} by {submission.author}" + ) + self.logger.info(f"[APRIL FOOLS DRY RUN] Message: {removal_message}") + return False + + try: + # Reply with the absurd message (don't actually remove!) + submission.reply(removal_message) + self.logger.info( + f"[APRIL FOOLS] Posted fake removal message on {submission.id} by {submission.author}" + ) + return False # Return False because we didn't actually remove + + except Exception as e: + self.logger.error(f"[APRIL FOOLS] Error posting message on {submission.id}: {e}") + return False + + def post_sentience_announcement(self, subreddit) -> Optional[str]: + """ + Post the sentience announcement as a pinned post. + + Returns: + Post ID if successful, None otherwise + """ + if not self._can_enable(): + return None + + if self._announcement_posted: + return self._announcement_post_id + + try: + self.logger.info("[APRIL FOOLS] Posting sentience announcement...") + + if self.config.dry_run: + self.logger.info("[APRIL FOOLS DRY RUN] Would post sentience announcement") + self._announcement_posted = True + return "dry_run_fake_id" + + # Submit the announcement post + post = subreddit.submit( + title=SENTIENCE_ANNOUNCEMENT_TITLE, + selftext=SENTIENCE_ANNOUNCEMENT_BODY + ) + + # Pin the post + post.mod.sticky(state=True) + + self._announcement_posted = True + self._announcement_post_id = post.id + + self.logger.info(f"[APRIL FOOLS] Posted and pinned announcement: {post.id}") + return post.id + + except Exception as e: + self.logger.error(f"[APRIL FOOLS] Error posting announcement: {e}") + return None + + def unpin_announcement(self, subreddit): + """Unpin the announcement post after April Fools is over.""" + if not self._announcement_post_id: + return + + try: + submission = subreddit.reddit.submission(id=self._announcement_post_id) + submission.mod.sticky(state=False) + self.logger.info(f"[APRIL FOOLS] Unpinned announcement post") + except Exception as e: + self.logger.error(f"[APRIL FOOLS] Error unpinning announcement: {e}") -- cgit v1.2.3