Initial commit
This commit is contained in:
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
10
.idea/ChristmanEvent.iml
generated
Normal file
10
.idea/ChristmanEvent.iml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.10 (ChristmanEvent)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
7
.idea/dictionaries/project.xml
generated
Normal file
7
.idea/dictionaries/project.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="project">
|
||||
<words>
|
||||
<w>flaired</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
20
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
20
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyMethodMayBeStaticInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="N813" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredIdentifiers">
|
||||
<list>
|
||||
<option value="google.genai" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.10 (ChristmanEvent)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (ChristmanEvent)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/ChristmanEvent.iml" filepath="$PROJECT_DIR$/.idea/ChristmanEvent.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
442
ChristmasEvent.py
Executable file
442
ChristmasEvent.py
Executable file
@@ -0,0 +1,442 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import praw
|
||||
import sqlite3
|
||||
import time
|
||||
import datetime
|
||||
import re
|
||||
|
||||
import config
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# DATABASE MANAGER
|
||||
# ==============================================================================
|
||||
|
||||
class DatabaseManager:
|
||||
def __init__(self, db_name="christmas_event.db"):
|
||||
self.conn = sqlite3.connect(db_name, check_same_thread=False)
|
||||
self.cursor = self.conn.cursor()
|
||||
self.create_tables()
|
||||
|
||||
def create_tables(self):
|
||||
# Participants table
|
||||
self.cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS participants (
|
||||
username TEXT PRIMARY KEY,
|
||||
joined_at REAL
|
||||
)
|
||||
''')
|
||||
|
||||
# Wishes table
|
||||
self.cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS wishes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
recipient TEXT,
|
||||
sender TEXT,
|
||||
message TEXT,
|
||||
received_at REAL,
|
||||
FOREIGN KEY(recipient) REFERENCES participants(username),
|
||||
UNIQUE(sender, recipient)
|
||||
)
|
||||
''')
|
||||
|
||||
# Conversations table
|
||||
self.cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS conversations (
|
||||
sender TEXT PRIMARY KEY,
|
||||
state TEXT,
|
||||
pending_recipient TEXT,
|
||||
pending_message TEXT,
|
||||
timestamp REAL
|
||||
)
|
||||
''')
|
||||
|
||||
# Tracks if the final gift has been sent to a user
|
||||
self.cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS deliveries (
|
||||
username TEXT PRIMARY KEY,
|
||||
delivered_at TIMESTAMP
|
||||
)
|
||||
''')
|
||||
self.conn.commit()
|
||||
|
||||
def add_participant(self, username):
|
||||
try:
|
||||
self.cursor.execute("INSERT INTO participants VALUES (?, ?)",
|
||||
(username, datetime.datetime.now()))
|
||||
self.conn.commit()
|
||||
return True
|
||||
except sqlite3.IntegrityError:
|
||||
return False
|
||||
|
||||
def save_wish(self, sender, recipient, message):
|
||||
"""Inserts or Replaces a wish"""
|
||||
self.cursor.execute("SELECT username FROM participants WHERE username = ?", (recipient,))
|
||||
if not self.cursor.fetchone():
|
||||
self.add_participant(recipient)
|
||||
|
||||
self.cursor.execute('''
|
||||
INSERT OR REPLACE INTO wishes (recipient, sender, message, received_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (recipient, sender, message, datetime.datetime.now()))
|
||||
self.conn.commit()
|
||||
return True
|
||||
|
||||
def check_existing_wish(self, sender, recipient):
|
||||
self.cursor.execute("SELECT message FROM wishes WHERE sender = ? AND recipient = ?", (sender, recipient))
|
||||
result = self.cursor.fetchone()
|
||||
return result[0] if result else None
|
||||
|
||||
def get_forest_data(self):
|
||||
self.cursor.execute('''
|
||||
SELECT p.username, COUNT(w.id) as wish_count
|
||||
FROM participants p
|
||||
LEFT JOIN wishes w ON p.username = w.recipient
|
||||
GROUP BY p.username
|
||||
ORDER BY p.username ASC
|
||||
''')
|
||||
return self.cursor.fetchall()
|
||||
|
||||
def set_conversation_state(self, sender, state, recipient, message):
|
||||
self.cursor.execute('''
|
||||
INSERT OR REPLACE INTO conversations (sender, state, pending_recipient, pending_message, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (sender, state, recipient, message, datetime.datetime.now()))
|
||||
self.conn.commit()
|
||||
|
||||
def get_conversation(self, sender):
|
||||
self.cursor.execute("SELECT state, pending_recipient, pending_message FROM conversations WHERE sender = ?",
|
||||
(sender,))
|
||||
return self.cursor.fetchone()
|
||||
|
||||
def clear_conversation(self, sender):
|
||||
self.cursor.execute("DELETE FROM conversations WHERE sender = ?", (sender,))
|
||||
self.conn.commit()
|
||||
|
||||
def get_undelivered_users(self):
|
||||
"""Returns list of usernames who have wishes but haven't received them yet."""
|
||||
self.cursor.execute('''
|
||||
SELECT DISTINCT p.username
|
||||
FROM participants p
|
||||
JOIN wishes w ON p.username = w.recipient
|
||||
LEFT JOIN deliveries d ON p.username = d.username
|
||||
WHERE d.username IS NULL
|
||||
''')
|
||||
return [row[0] for row in self.cursor.fetchall()]
|
||||
|
||||
def get_user_wishes(self, username):
|
||||
self.cursor.execute("SELECT sender, message FROM wishes WHERE recipient = ?", (username,))
|
||||
return self.cursor.fetchall()
|
||||
|
||||
def mark_delivered(self, username):
|
||||
self.cursor.execute("INSERT INTO deliveries VALUES (?, ?)", (username, datetime.datetime.now()))
|
||||
self.conn.commit()
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# SAFETY FILTER
|
||||
# ==============================================================================
|
||||
|
||||
class SafetyFilter:
|
||||
def __init__(self, blocklist):
|
||||
self.blocklist = blocklist
|
||||
|
||||
def is_safe(self, text):
|
||||
"""
|
||||
Returns True if text is safe, False if it contains blocked terms.
|
||||
Performs a simple substring check (case-insensitive).
|
||||
"""
|
||||
if not text:
|
||||
return True
|
||||
|
||||
text_lower = text.lower()
|
||||
for bad_word in self.blocklist:
|
||||
# Check if the bad word exists in the text
|
||||
# Note: logic can be improved with regex \b word boundaries if needed
|
||||
if bad_word in text_lower:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# BOT LOGIC
|
||||
# ==============================================================================
|
||||
|
||||
class ChristmasBot:
|
||||
def __init__(self):
|
||||
print("🎄 Initializing Christmas Helper...")
|
||||
self.reddit = praw.Reddit(
|
||||
client_id=config.eventBotClientId,
|
||||
client_secret=config.eventBotSecret,
|
||||
user_agent=config.user_agent,
|
||||
username=config.username,
|
||||
password=config.password
|
||||
)
|
||||
self.reddit.validate_on_submit = True
|
||||
self.db = DatabaseManager()
|
||||
self.safety = SafetyFilter(config.blocklist)
|
||||
self.subreddit = self.reddit.subreddit(config.subreddit_name)
|
||||
self.last_dashboard_update = 0
|
||||
|
||||
self.username_pattern = re.compile(r"^u/[A-Za-z0-9_-]{3,20}$", re.IGNORECASE)
|
||||
self.yes_pattern = re.compile(r"^(yes|y|yeah|yup|ja|si|oui|confirm)$", re.IGNORECASE)
|
||||
self.no_pattern = re.compile(r"^(no|nop|cancel|nah|not quite|negative|nuh uh)$", re.IGNORECASE)
|
||||
|
||||
def get_visual_icon(self, count):
|
||||
if count == 0:
|
||||
return "🌱"
|
||||
elif count == 1:
|
||||
return "🎄 (1 Gift)"
|
||||
elif count <= 5:
|
||||
return "🎄" * count + f" ({count})"
|
||||
else:
|
||||
return f"🌟🌟🌟 ({count} Gifts!)"
|
||||
|
||||
def verify_user_flair(self, username):
|
||||
try:
|
||||
flair_list = list(self.subreddit.flair(redditor=username))
|
||||
if not flair_list: return False
|
||||
user_flair = flair_list[0]
|
||||
if user_flair['flair_text'] or user_flair['flair_css_class']: return True
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error checking flair for {username}: {e}")
|
||||
return False
|
||||
|
||||
def update_dashboard(self):
|
||||
now = time.time()
|
||||
if now - self.last_dashboard_update < config.update_post_interval: return
|
||||
|
||||
print("📝 Updating Forest Dashboard...")
|
||||
data = self.db.get_forest_data()
|
||||
|
||||
body = "# 🎄 The Christmas Forest 🎄\n\n"
|
||||
body += "### How to Participate:\n"
|
||||
body += "1. Send a DM to u/{}\n".format(config.username)
|
||||
body += "2. **First Line:** The username (MUST start with `u/` e.g. `u/SexyElf69`)\n"
|
||||
body += "3. **Next Lines:** Your message.\n\n"
|
||||
body += "**Note:** The bot will ask you to confirm before saving.\n"
|
||||
body += "---\n\n"
|
||||
body += "| User | Tree Status |\n| :--- | :--- |\n"
|
||||
|
||||
if not data: body += "| The Forest is quiet... | Be the first to send a wish! |\n"
|
||||
for username, count in data:
|
||||
icon = self.get_visual_icon(count)
|
||||
body += f"| u/{username} | {icon} |\n"
|
||||
|
||||
body += "\n\n---\n*Updated: " + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "*"
|
||||
|
||||
try:
|
||||
submission = self.reddit.submission(id=config.master_post_id)
|
||||
submission.edit(body, )
|
||||
self.last_dashboard_update = now
|
||||
print("✅ Dashboard updated.")
|
||||
except Exception as e:
|
||||
print(f"❌ Error updating dashboard: {e}")
|
||||
|
||||
def parse_new_wish_request(self, message):
|
||||
body_lines = message.body.strip().split('\n')
|
||||
body_lines = [line.strip() for line in body_lines if line.strip()]
|
||||
|
||||
if not body_lines:
|
||||
return None, None
|
||||
|
||||
first_line = body_lines[0]
|
||||
|
||||
if self.username_pattern.match(first_line):
|
||||
target_user = first_line[2:]
|
||||
wish_content = '\n'.join(body_lines[1:])
|
||||
return target_user, wish_content
|
||||
|
||||
return None, None
|
||||
|
||||
def process_inbox(self):
|
||||
try:
|
||||
for message in self.reddit.inbox.unread():
|
||||
if message.was_comment: continue
|
||||
|
||||
sender = message.author.name if message.author else "Unknown"
|
||||
print(f"📩 Message from {sender}")
|
||||
|
||||
conversation = self.db.get_conversation(sender)
|
||||
|
||||
if conversation:
|
||||
self.handle_confirmation(message, sender, conversation)
|
||||
else:
|
||||
self.handle_new_request(message, sender)
|
||||
|
||||
message.mark_read()
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error checking inbox: {e}")
|
||||
|
||||
def handle_new_request(self, message, sender):
|
||||
target_user, wish_content = self.parse_new_wish_request(message)
|
||||
|
||||
# ERROR: Bad Format
|
||||
if not target_user:
|
||||
reply = ("I couldn't understand your request. Please format your message exactly like this:\n\n"
|
||||
"`u/TargetUsername`\n\n"
|
||||
"`Your message here...`\n\n"
|
||||
"Make sure the username is on the **first line** and starts with **u/**.")
|
||||
message.reply(reply)
|
||||
return
|
||||
|
||||
# ERROR: Empty Body
|
||||
if not wish_content:
|
||||
message.reply(f"You didn't write a message for u/{target_user}! Please try again with text in the body.")
|
||||
return
|
||||
|
||||
# SAFETY CHECK
|
||||
if not self.safety.is_safe(wish_content):
|
||||
print(f"⛔ Blocked unsafe content from {sender}")
|
||||
message.reply("⛔ **Message Rejected**\n\n"
|
||||
"Your message contains words or phrases that are not allowed in this event. "
|
||||
"Please ensure your wish is kind and follows the community guidelines.")
|
||||
return
|
||||
|
||||
# ERROR: No Flair
|
||||
if not self.verify_user_flair(target_user):
|
||||
message.reply(f"Sorry, u/{target_user} does not have a User Flair in r/{config.subreddit_name}.\n"
|
||||
"We only allow wishes for active, flaired community members.")
|
||||
return
|
||||
|
||||
# CHECK: Existing Wish
|
||||
existing_msg = self.db.check_existing_wish(sender, target_user)
|
||||
|
||||
if existing_msg:
|
||||
self.db.set_conversation_state(sender, "CONFIRM_REPLACE", target_user, wish_content)
|
||||
reply = (f"⚠️ **You already sent a wish to u/{target_user}.**\n\n"
|
||||
f"**Old Message:** {existing_msg[:100]}...\n\n"
|
||||
f"**Do you want to REPLACE it with:**\n"
|
||||
f"> {wish_content}\n\n"
|
||||
f"Reply **YES** to confirm or **NO** to cancel.")
|
||||
message.reply(reply)
|
||||
else:
|
||||
self.db.set_conversation_state(sender, "CONFIRM_NEW", target_user, wish_content)
|
||||
reply = (f"🎄 **Christmas Confirmation** 🎄\n\n"
|
||||
f"I extracted that you want to send a wish to **u/{target_user}**.\n\n"
|
||||
f"**Message:**\n"
|
||||
f"> {wish_content}\n\n"
|
||||
f"Is this correct? Reply **YES** to save or **NO** to cancel.")
|
||||
message.reply(reply)
|
||||
|
||||
def handle_confirmation(self, message, sender, conversation):
|
||||
state, target, content = conversation
|
||||
user_response = message.body.strip().lower()
|
||||
|
||||
# SAFETY CHECK (Re-check in case DB state was manipulated or rules changed)
|
||||
if not self.safety.is_safe(content):
|
||||
self.db.clear_conversation(sender)
|
||||
message.reply("⛔ **Message Rejected** during confirmation. Content violates safety rules.")
|
||||
return
|
||||
|
||||
if self.yes_pattern.match(user_response):
|
||||
self.db.save_wish(sender, target, content)
|
||||
self.db.clear_conversation(sender)
|
||||
|
||||
print(f"💾 Wish saved for {target}")
|
||||
if state == "CONFIRM_REPLACE":
|
||||
message.reply(f"✅ Your wish for u/{target} has been **updated**! 🎄")
|
||||
print(f"{sender} has updated their wish for u/{target} to **{content}**.")
|
||||
else:
|
||||
message.reply(f"✅ Your wish for u/{target} has been **saved**! 🎄")
|
||||
print(f"{sender} has saved their wish for u/{target}: **{content}**.")
|
||||
|
||||
elif self.no_pattern.match(user_response):
|
||||
self.db.clear_conversation(sender)
|
||||
message.reply("❌ Cancelled. You can send a new message to start over.")
|
||||
print(f"🚫 Action cancelled by {sender}")
|
||||
|
||||
else:
|
||||
message.reply("I didn't catch that. Please reply **YES** to confirm or **NO** to cancel.")
|
||||
|
||||
def run(self):
|
||||
print("🚀 Christmas Bot Started. Press Ctrl+C to stop.")
|
||||
while True:# 1. Process new wishes
|
||||
self.process_inbox()
|
||||
|
||||
# 2. Update the public post
|
||||
self.update_dashboard()
|
||||
|
||||
# 3. Check if it's Christmas yet (NEW)
|
||||
self.check_distribution()
|
||||
|
||||
time.sleep(config.check_interval)
|
||||
|
||||
def check_distribution(self):
|
||||
"""Checks if it is Dec 24th UTC (Midnight) or later and triggers distribution."""
|
||||
now_utc = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
# Check: Is it December AND is it the 24th or later?
|
||||
if now_utc.month == 12 and now_utc.day >= 24:
|
||||
self.distribute_gifts()
|
||||
|
||||
def distribute_gifts(self):
|
||||
recipients = self.db.get_undelivered_users()
|
||||
|
||||
# Silent exit if no pending deliveries (prevents log spam)
|
||||
if not recipients:
|
||||
return
|
||||
|
||||
print(f"🎅 HO HO HO! It's Christmas! Distributing gifts to {len(recipients)} users...")
|
||||
|
||||
for username in recipients:
|
||||
wishes = self.db.get_user_wishes(username)
|
||||
if not wishes: continue
|
||||
|
||||
print(f"🎁 Preparing gifts for u/{username}...")
|
||||
|
||||
# --- PAGINATION LOGIC ---
|
||||
messages_to_send = []
|
||||
current_part_content = ""
|
||||
current_part_num = 1
|
||||
|
||||
# Header for first message
|
||||
header = f"Merry Christmas u/{username}!\n\nThe wait is over. Here are your wishes:\n\n---\n\n"
|
||||
current_part_content += header
|
||||
|
||||
for sender, message_text in wishes:
|
||||
# Format the single wish
|
||||
wish_block = f"**From: u/{sender}**\n> {message_text}\n\n---\n\n"
|
||||
|
||||
# Check character limit (safe buffer of 9000 chars)
|
||||
if len(current_part_content) + len(wish_block) > 9000:
|
||||
# Current part full. Close it and start new one.
|
||||
current_part_content += f"\n*(Continued in Part {current_part_num + 1}...)*"
|
||||
messages_to_send.append(current_part_content)
|
||||
|
||||
current_part_num += 1
|
||||
current_part_content = f"**Christmas Wishes Part {current_part_num}**\n\n---\n\n"
|
||||
current_part_content += wish_block
|
||||
else:
|
||||
current_part_content += wish_block
|
||||
|
||||
# Append the footer to the last part
|
||||
current_part_content += f"\nYou received {len(wishes)} ornament(s). Have a wonderful holiday!\n"
|
||||
current_part_content += f"\n*Automated message from r/{config.subreddit_name}*"
|
||||
messages_to_send.append(current_part_content)
|
||||
|
||||
# --- SENDING LOOP ---
|
||||
try:
|
||||
for index, body in enumerate(messages_to_send):
|
||||
subject_line = "🎄 Open your Christmas Time Capsule! 🎁"
|
||||
if len(messages_to_send) > 1:
|
||||
subject_line += f" (Part {index + 1}/{len(messages_to_send)})"
|
||||
|
||||
self.reddit.redditor(username).message(subject=subject_line, message=body)
|
||||
# Small sleep between parts to ensure order and avoid spam trigger
|
||||
time.sleep(2)
|
||||
|
||||
self.db.mark_delivered(username)
|
||||
print(f"✅ Delivered {len(messages_to_send)} part(s) to u/{username}")
|
||||
time.sleep(5) # Delay between USERS
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to send to u/{username}: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
bot = ChristmasBot()
|
||||
bot.run()
|
||||
248
StatsHelper.py
Normal file
248
StatsHelper.py
Normal file
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python3
|
||||
import praw
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
import config
|
||||
|
||||
# --- SETTINGS ---
|
||||
START_YEAR = 2024
|
||||
START_MONTH = 12 # December
|
||||
|
||||
AVAILABLE_FLAIRS=[
|
||||
"Moderator Announcement 📢",
|
||||
"Horny😈",
|
||||
"Appreciation ☺️",
|
||||
"Meme!😋",
|
||||
"Lore📚✍️",
|
||||
"Hunter😈",
|
||||
"Truth or Dare - NNYC Edition - 🥵",
|
||||
"Truth or Dare - NNYC Edition - 💞",
|
||||
"Triumph Diary",
|
||||
"Defeat Diary",
|
||||
"cursed meme🙃👿",
|
||||
"Gamble 🎰🎲🃏",
|
||||
"DDD Challenge",
|
||||
"Holly Jolly Time 🎄⛄🦌🎁🍪🌟",
|
||||
"Happy New Year 🎊🥂🎉🍀",
|
||||
"Easter Egg-Stravaganza 🐰🐇",
|
||||
"[Game] - Would you rather...⁉️",
|
||||
":pokeball::pokeball: Gotta catch 'em all :pokeball::pokeball:",
|
||||
"Will you be my Valentine 💖❤️💌"
|
||||
]
|
||||
|
||||
class StatsHelper:
|
||||
|
||||
def __init__(self):
|
||||
print("🎄 Initializing StatsHelper...")
|
||||
|
||||
def get_next_month_start(self, year, month):
|
||||
"""Helper to calculate the first day of the NEXT month."""
|
||||
if month == 12:
|
||||
return year + 1, 1
|
||||
else:
|
||||
return year, month + 1
|
||||
|
||||
def process_batch(self, batch_name, post_list):
|
||||
"""
|
||||
Helper function to process a list of posts and print stats.
|
||||
"""
|
||||
print(f"\n🔵 PROCESSING: {batch_name} (Limit: ~1000)")
|
||||
print("-" * 60)
|
||||
|
||||
unknown_flairs = Counter()
|
||||
matched_count = 0
|
||||
no_flair_count = 0
|
||||
total_scanned = 0
|
||||
|
||||
for post in post_list:
|
||||
total_scanned += 1
|
||||
flair = post.link_flair_text
|
||||
|
||||
# Check 1: No Flair
|
||||
if not flair:
|
||||
no_flair_count += 1
|
||||
continue
|
||||
|
||||
# Check 2: Known List
|
||||
if flair in AVAILABLE_FLAIRS:
|
||||
matched_count += 1
|
||||
else:
|
||||
unknown_flairs[flair] += 1
|
||||
|
||||
# --- REPORT FOR THIS BATCH ---
|
||||
print(f"Total Retrieved: {total_scanned}")
|
||||
print(f"✅ Known Flairs: {matched_count}")
|
||||
print(f"👻 No Flair: {no_flair_count}")
|
||||
|
||||
if unknown_flairs:
|
||||
print(f"⚠️ UNKNOWN FLAIRS: {sum(unknown_flairs.values())}")
|
||||
print(f" (Check these for typos/spaces)")
|
||||
for f, c in unknown_flairs.most_common():
|
||||
# Use repr() to see hidden spaces, e.g. 'Meme '
|
||||
print(f" {repr(f):<40} : {c}")
|
||||
else:
|
||||
print("🎉 No unknown flairs found.")
|
||||
print("-" * 60)
|
||||
|
||||
def analyze(self):
|
||||
reddit = praw.Reddit(
|
||||
client_id=config.eventBotClientId,
|
||||
client_secret=config.eventBotSecret,
|
||||
user_agent=config.user_agent,
|
||||
username=config.megathread_username,
|
||||
password=config.megathread_password
|
||||
)
|
||||
# Trackers
|
||||
unknown_flairs = Counter()
|
||||
no_flair_count = 0
|
||||
matched_count = 0
|
||||
|
||||
subreddit = reddit.subreddit(config.subreddit_name)
|
||||
|
||||
# # We use a dictionary of Counters:
|
||||
# # Structure: { "2023-10": Counter({'FlairA': 10, 'FlairB': 2}), "2023-11": ... }
|
||||
# monthly_stats = defaultdict(Counter)
|
||||
# # We will also create a counter for the TOTAL stats across all months
|
||||
# grand_total_stats = Counter()
|
||||
#
|
||||
# print(f"Starting precise calendar analysis for r/{config.subreddit_name}...")
|
||||
#
|
||||
# total_posts_processed = 0
|
||||
#
|
||||
# for available_flair in AVAILABLE_FLAIRS:
|
||||
# # Prepare the query
|
||||
# # syntax='cloudsearch' requires the timestamp field
|
||||
# # include_over_18 ensures we see posts even if sub is flagged mature
|
||||
# query = f'flair:"{available_flair}"'
|
||||
# print(f"Checking {available_flair}...")
|
||||
#
|
||||
# try:
|
||||
# posts = list(subreddit.search(
|
||||
# query,
|
||||
# limit=None,
|
||||
# syntax="cloudsearch",
|
||||
# params={'include_over_18': 'on'}
|
||||
# ))
|
||||
#
|
||||
# overall_count = len(posts)
|
||||
# print(f"Found {overall_count} posts for {available_flair}...")
|
||||
#
|
||||
# count_for_flair = 0
|
||||
# for post in posts:
|
||||
# # 2. Convert timestamp to "YYYY-MM" string key
|
||||
# dt = datetime.fromtimestamp(post.created_utc, timezone.utc)
|
||||
# month_key = dt.strftime('%Y-%m')
|
||||
#
|
||||
# # 3. Add to stats
|
||||
# # We use 'available_flair' to ensure the naming is consistent
|
||||
# # (even if the post has slightly messed up encoding)
|
||||
# monthly_stats[month_key][available_flair] += 1
|
||||
# # 4. Add to Grand Total stats
|
||||
# grand_total_stats[available_flair] += 1
|
||||
# total_posts_processed += 1
|
||||
# count_for_flair += 1
|
||||
#
|
||||
# if count_for_flair == 0:
|
||||
# print("⚠️ (0 in range)")
|
||||
# else:
|
||||
# print(f"✅ ({count_for_flair} found)")
|
||||
#
|
||||
# except Exception as e:
|
||||
# print(f"\n❌ Error: {e}")
|
||||
#
|
||||
# # --- OUTPUT RESULTS ---
|
||||
# print("\n" + "=" * 60)
|
||||
# print(f"FINAL REPORT: {total_posts_processed} POSTS ANALYZED")
|
||||
# print("=" * 60)
|
||||
#
|
||||
# # Sort keys to ensure chronological printing (2024-12, then 2025-01)
|
||||
# sorted_months = sorted(monthly_stats.keys())
|
||||
#
|
||||
# if not sorted_months:
|
||||
# print("No posts found in the specified timeframe.")
|
||||
#
|
||||
# for month_key in sorted_months:
|
||||
# print(f"\n📅 PERIOD: {month_key}")
|
||||
# print(f"{'FLAIR':<40} | {'COUNT':<5} | {'%'}")
|
||||
# print("-" * 60)
|
||||
#
|
||||
# total_in_month = sum(monthly_stats[month_key].values())
|
||||
#
|
||||
# # Sort flairs by popularity within that month
|
||||
# for flair, count in monthly_stats[month_key].most_common():
|
||||
# percentage = (count / total_in_month) * 100
|
||||
#
|
||||
# # Truncate flair name if it's too long for the table
|
||||
# display_flair = (flair[:37] + '..') if len(flair) > 37 else flair
|
||||
#
|
||||
# print(f"{display_flair:<40} | {count:<5} | {percentage:.1f}%")
|
||||
#
|
||||
# # --- OUTPUT 2: GRAND TOTAL SUMMARY ---
|
||||
# print("\n" + "=" * 60)
|
||||
# print(f"PART 2: GRAND TOTAL SUMMARY (ALL MONTHS COMBINED)")
|
||||
# print(f"Total Posts: {total_posts_processed}")
|
||||
# print("=" * 60)
|
||||
#
|
||||
# print(f"\n📅 PERIOD: ALL TIME ({sorted_months[0]} to {sorted_months[-1]})")
|
||||
# print(f"{'FLAIR':<40} | {'COUNT':<5} | {'%'}")
|
||||
# print("-" * 60)
|
||||
#
|
||||
# for flair, count in grand_total_stats.most_common():
|
||||
# # Percentage relative to the TOTAL posts analyzed across the whole period
|
||||
# percentage = (count / total_posts_processed) * 100
|
||||
#
|
||||
# display_flair = (flair[:37] + '..') if len(flair) > 37 else flair
|
||||
# print(f"{display_flair:<40} | {count:<5} | {percentage:.1f}%")
|
||||
|
||||
|
||||
#One more search for potentially missed posts
|
||||
try:
|
||||
# print("\nFetching .new() ...")
|
||||
# posts_new = list(subreddit.new(limit=None))
|
||||
# self.process_batch("SUBREDDIT.NEW", posts_new)
|
||||
#
|
||||
# # 2. FETCH HOT (Algorithm sorted, max ~1000)
|
||||
# print("\nFetching .hot() ...")
|
||||
# posts_hot = list(subreddit.hot(limit=None))
|
||||
# self.process_batch("SUBREDDIT.HOT", posts_hot)
|
||||
#
|
||||
# # 2. FETCH TOP (Algorithm sorted, max ~1000)
|
||||
# print("\nFetching .top() ...")
|
||||
# posts_hot = list(subreddit.top(limit=None))
|
||||
# self.process_batch("SUBREDDIT.TOP", posts_hot)
|
||||
#
|
||||
# # 2. FETCH CONTROVERSIAL (Algorithm sorted, max ~1000)
|
||||
# print("\nFetching .controversial() ...")
|
||||
# posts_hot = list(subreddit.controversial(limit=None))
|
||||
# self.process_batch("SUBREDDIT.CONTROVERSIAL", posts_hot)
|
||||
#
|
||||
# # 2. FETCH RISING (Algorithm sorted, max ~1000)
|
||||
# print("\nFetching .rising() ...")
|
||||
# posts_hot = list(subreddit.rising(limit=None))
|
||||
# self.process_batch("SUBREDDIT.RISING", posts_hot)
|
||||
|
||||
random_counter=0
|
||||
print("\nFetching .random() ...")
|
||||
while random_counter < 10000:
|
||||
rand_post = subreddit.random()
|
||||
|
||||
flair = rand_post.link_flair_text
|
||||
|
||||
# Check 1: No Flair
|
||||
if not flair:
|
||||
print(f"Found unflaired post => {rand_post.id}")
|
||||
continue
|
||||
|
||||
# Check 2: Known List
|
||||
if not flair in AVAILABLE_FLAIRS:
|
||||
print(f"Found unexpected flair => {flair}")
|
||||
|
||||
if random_counter % 10 == 0:
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
BIN
__pycache__/StatsHelper.cpython-310.pyc
Normal file
BIN
__pycache__/StatsHelper.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/config.cpython-310.pyc
Normal file
BIN
__pycache__/config.cpython-310.pyc
Normal file
Binary file not shown.
BIN
christmas_event.db
Normal file
BIN
christmas_event.db
Normal file
Binary file not shown.
102
config.py
Normal file
102
config.py
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
#The client_id of the EventBot ass set in https://www.reddit.com/prefs/apps
|
||||
eventBotClientId='helOP0151Te4ytmwHoG6jg'
|
||||
#The client_secret of the EventBot ass set in https://www.reddit.com/prefs/apps
|
||||
eventBotSecret='JbBOr8-FbgDc8-S2kD5NE4DdTqMddA'
|
||||
#User chosen unique identifier for this agent.
|
||||
user_agent='linux:Event-Bot by u/droid_tect v0.1'
|
||||
#The user name to post under
|
||||
username='NNYC-EventBot'
|
||||
#The password of the user name to post under.
|
||||
password='q}7Y~6f-xNzLdBt'
|
||||
#The name of the Subreddit to carry out this event on
|
||||
#subreddit_name="EventBotTestingGround"
|
||||
subreddit_name="NoNutYearlyCommunity"
|
||||
#The ID of the "Master Post" to keep track of the wishes
|
||||
master_post_id="1p5dtbr"
|
||||
#Interval in seconds to check for new messages
|
||||
check_interval=60
|
||||
#Interval in seconds to update the dashboard if something changed
|
||||
update_post_interval=300
|
||||
|
||||
megathread_username="droid_tect"
|
||||
megathread_password="lwwh1b-9gK-Ds_o"
|
||||
|
||||
# SAFETY NET: Lowercase words/phrases that trigger immediate rejection
|
||||
# You can expand this list with specific slurs or harassment terms relevant to your community.
|
||||
blocklist=[
|
||||
# --- DIRECT THREATS & SELF-HARM ENCOURAGEMENT ---
|
||||
"kill yourself",
|
||||
"kys",
|
||||
"hang yourself",
|
||||
"drink bleach",
|
||||
"hope you die",
|
||||
"go die",
|
||||
"suicide",
|
||||
"commit suicide",
|
||||
"die in a fire",
|
||||
"cut yourself",
|
||||
|
||||
# --- COMMON HARASSMENT & INSULTS ---
|
||||
# In an 18+ sub, you might tolerate "trashy", but these cross the line
|
||||
"you are disgusting",
|
||||
"you look disgusting",
|
||||
"ugly",
|
||||
"fat pig",
|
||||
"whale",
|
||||
"vomit",
|
||||
"waste of space",
|
||||
"waste of oxygen",
|
||||
"nobody loves you",
|
||||
"nobody likes you",
|
||||
"incel",
|
||||
"femcel",
|
||||
"neckbeard",
|
||||
"retard", # Ableist slur
|
||||
"retarded",
|
||||
|
||||
# --- SERIOUS ACCUSATIONS (DRAMA/TROLLING) ---
|
||||
"pedophile",
|
||||
"pedo",
|
||||
"groomer",
|
||||
"rapist",
|
||||
"scammer",
|
||||
"animal abuser",
|
||||
|
||||
# --- HATE SPEECH & EXTREMISM ---
|
||||
# (Please add specific racial/homophobic slurs from your AutoMod here)
|
||||
"nazi",
|
||||
"hitler",
|
||||
"white power",
|
||||
"white supremacy",
|
||||
"black supremacy",
|
||||
|
||||
# --- SPAM & SCAMS ---
|
||||
"bitcoin",
|
||||
"crypto",
|
||||
"giveaway",
|
||||
"telegram",
|
||||
"whatsapp",
|
||||
"investment",
|
||||
"forex",
|
||||
"nft",
|
||||
"wallet",
|
||||
"cashapp",
|
||||
"venmo",
|
||||
"paypal",
|
||||
"buy my",
|
||||
"selling",
|
||||
"discount",
|
||||
"promo code",
|
||||
"click here",
|
||||
"check out my onlyfans", # Depending on if you allow self-promo in wishes
|
||||
"subscribe to",
|
||||
|
||||
# --- DOXXING ---
|
||||
"real name is",
|
||||
"lives at",
|
||||
"phone number",
|
||||
"address is",
|
||||
"ip address",
|
||||
]
|
||||
BIN
pictures/1201_1.png
Normal file
BIN
pictures/1201_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
pictures/1201_2.png
Normal file
BIN
pictures/1201_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
78
stats_config.py
Normal file
78
stats_config.py
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
base_title="[Day {day_of_month}] Advent Calendar: {topic_title} 🎄"
|
||||
|
||||
base_message="""🎅 **Ho Ho Ho, Community!**
|
||||
|
||||
Welcome to **Day {day_of_month}** of our **Subreddit Advent Calendar**! We are counting down to the big holiday with a new prompt every single day.
|
||||
|
||||
# 🌟 Today's Theme:
|
||||
|
||||
{topic_description}
|
||||
|
||||
# 🎁 How to Participate
|
||||
|
||||
The rules are simple: **Interpret the theme however you want!**
|
||||
|
||||
* **Wholesome?** ✅ (e.g., Share a recipe, a childhood memory, a cozy SFW pic, or a meme.)
|
||||
* **Spicy (+18)?** ✅ (e.g., NSFW stories, artistic photos, or "creative" interpretations. **Please ensure all NSFW content is tagged appropriately!**)
|
||||
* **Off-the-wall?** ✅ (Surprise us!)
|
||||
|
||||
Post your submissions in the comments below or to the subreddit! Let's fill this thread and the subreddit with holiday spirit.
|
||||
|
||||
*(Reminder: Keep it respectful. All standard subreddit rules regarding consent and harassment apply.)*
|
||||
"""
|
||||
|
||||
topic_titles=[
|
||||
"Deck the Halls",
|
||||
"Baby It's Cold Outside",
|
||||
"Sugar & Spice",
|
||||
"Code Red",
|
||||
"Naughty or Nice?",
|
||||
"Stocking Stuffers",
|
||||
"Under Wraps",
|
||||
"Jingle Bell Rock",
|
||||
"Lights Out",
|
||||
"Santa’s Helpers",
|
||||
"Comfort & Joy",
|
||||
"Mid-Month Madness",
|
||||
"Winter Whiteout",
|
||||
"Toy Story",
|
||||
"Sweater Weather",
|
||||
"Silent Night",
|
||||
"All That Glitters",
|
||||
"Unboxing",
|
||||
"Spirits & Spirits",
|
||||
"Mistletoe",
|
||||
"Jack Frost",
|
||||
"The Night Before",
|
||||
"Feast Mode",
|
||||
"The Grand Reveal"
|
||||
]
|
||||
|
||||
topic_descriptions=[
|
||||
"We’re kicking things off with decorations! Show us your Christmas tree setup, your festive room decor, or perhaps... how you’ve chosen to \"decorate\" yourself for the season. Tinsel, ornaments, and lights are encouraged—wherever you choose to hang them.",
|
||||
"The temperature is dropping, so show us how you stay warm. Are you curling up in fluffy socks and a blanket? Sipping hot cocoa by the fire? Or have you found a warmer way to generate some body heat? Let's get cozy.",
|
||||
"Today is all about the sweetness of the season. Share your favorite holiday cookie recipes, photos of your baking disasters, or show us that you are the sweetest treat in the house. Are you made of sugar, spice, or something nicer?",
|
||||
"Red is the color of the season. We want to see it! Red lipstick, red wine, red Santa hats, or perhaps some stunning red lingerie. If it’s rouge, crimson, or scarlet, it belongs in this thread.",
|
||||
"It’s time for a confession. Have you been an angel this year, volunteering and helping others? Or have you been remarkably wicked? Tell us a story of a good deed... or a very naughty one. Santa is listening.",
|
||||
"Happy St. Nicholas Day! Today we celebrate the \"Stocking.¸\" Whether that means discussing small, thoughtful gifts, showing off your favorite festive socks, or showcasing incredible legs in nylon stockings—fill the thread up!",
|
||||
"Wrapping gifts is an art form. Show us your perfectly wrapped presents (or your messy tape disasters). Alternatively, show us yourself all wrapped up—in ribbons, in a duvet, or waiting to be unwrapped by someone special.",
|
||||
"Let’s get loud! Post your favorite Christmas party anthems, a video of your terrible dancing, or a picture of you ready for a night out. How do you rock around the Christmas tree?",
|
||||
"When the sun goes down, the mood changes. We want to see glow-in-the-dark themes, fairy lights in a dark room, artistic silhouettes, or stories about what happens after the lights go out.",
|
||||
"Santa can’t do it all alone. Today celebrates the Elves! Show us your best Elf on the Shelf memes, your own Elf costume, or tell us a story about how you \"serviced\" the community or a partner recently.",
|
||||
"Mid-week relaxation is vital. What brings you pure joy? Is it a bubble bath, a glass of whiskey, a specific video game, or a massage? Share your self-care routines (SFW or NSFW) that keep the stress away.",
|
||||
"We are halfway there! Let’s blow off some steam. Post your funniest holiday memes, bloopers, or vent about the chaos of holiday shopping. Let’s laugh at the madness together.",
|
||||
"Dreaming of a White Christmas? We want to see snow! Real snow landscapes, white outfits, foam, bubbles, or... other white substances. Let’s turn this thread into a blizzard.",
|
||||
"Christmas is for toys! Gamers, show us your rigs. Collectors, show us your figures. And for our 18+ crowd... feel free to showcase your favorite toys that don't belong in a child's stocking. Reviews and recommendations welcome!",
|
||||
"The Ugly Christmas Sweater: a fashion icon. Post your most hideous knitwear. Or, if you’re feeling warm, show us what it looks like when the sweater comes off.",
|
||||
"Sometimes the best moments are the quiet ones. Share a story of a solo evening, a peaceful view, or the intimate, silent moments shared between two people. Shhh...",
|
||||
"Sparkle season is here. We want glitter, sequins, shiny jewelry, and glossy lips. If it reflects light and catches the eye, post it here. Let’s make this thread shine.",
|
||||
"The thrill of opening something new. Post a haul of gifts you’ve bought, an unboxing video of a new gadget, or a \"reveal\" photo set where you unbox... yourself.",
|
||||
"Cheers! Share your favorite mulled wine recipe, a photo of your holiday cocktail, or a funny story about a time you had a few too many spirits. Drink responsibly, post recklessly (within the rules)!",
|
||||
"Pucker up! Today is about romance and kissing. Share a wholesome couple pic, a story about a holiday crush, or something a bit more passionate happening under the greenery.",
|
||||
"It’s nippy out there. We’re exploring the sensation of cold. Ice cubes, cold showers, shivering in the snow, or how you warm up strictly after getting caught in the freeze.",
|
||||
"The anticipation is palpable. Are you excited? Nervous? Show us your prep work. Food prep, outfit prep, or preparing the bedroom for a guest. What are you doing right now to get ready?",
|
||||
"The big meal is coming. Post your food porn! Roasts, veggies, desserts... or describe a time you were the main course. Let’s get hungry.",
|
||||
"MERRY CHRISTMAS!\nToday is the day! The Bot is currently distributing all your secret wishes via DM. Use this thread to react to your messages, thank your anonymous well-wishers, and celebrate the finale of our event!"
|
||||
]
|
||||
Reference in New Issue
Block a user