Add bot.py

This commit is contained in:
2025-12-23 18:00:28 +00:00
parent 67fd33aff6
commit c410d47383

345
bot.py Normal file
View File

@@ -0,0 +1,345 @@
import discord
from discord import app_commands
from discord.ext import commands, tasks
import sqlite3
DB_FILE = "swearjar.db"
SWEAR_WORDS = {REDACTED FOR CODE REVIEW}
BOT_TOKEN = "REDACTED FOR CODE REVIEW"
intents = discord.Intents.default()
intents.message_content = True
intents.members = True
intents.guilds = True
# ==============================
# BOT CLASS
# ==============================
class SwearJarBot(commands.Bot):
def __init__(self):
super().__init__(command_prefix="!", intents=intents)
self.synced = False
async def setup_hook(self):
self.remove_command("help")
self.update_status.start()
async def on_ready(self):
init_db()
if not self.synced:
await self.tree.sync()
self.synced = True
print(f"✅ Logged in as {self.user} (ID: {self.user.id})")
@tasks.loop(minutes=10)
async def update_status(self):
"""Updates the bot's 'Listening to XXX swear words' every 10 minutes."""
total = get_global_swear_count()
activity = discord.Activity(
type=discord.ActivityType.listening,
name=f"{total:,} swear words"
)
await self.change_presence(activity=activity)
@update_status.before_loop
async def before_update_status(self):
await self.wait_until_ready()
bot = SwearJarBot()
# ==============================
# DATABASE FUNCTIONS
# ==============================
def init_db():
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
cur.execute(
"""CREATE TABLE IF NOT EXISTS swear_counts (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
count INTEGER DEFAULT 0,
PRIMARY KEY (guild_id, user_id)
)"""
)
cur.execute(
"""CREATE TABLE IF NOT EXISTS privacy_optouts (
user_id TEXT PRIMARY KEY
)"""
)
conn.commit()
conn.close()
def add_swear(guild_id: int, user_id: int, count: int = 1):
if is_user_opted_out(user_id):
return
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
cur.execute(
"""INSERT INTO swear_counts (guild_id, user_id, count)
VALUES (?, ?, ?)
ON CONFLICT(guild_id, user_id)
DO UPDATE SET count = count + ?""",
(str(guild_id), str(user_id), count, count)
)
conn.commit()
conn.close()
def get_swear_counts(user_id: int):
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
cur.execute("SELECT guild_id, count FROM swear_counts WHERE user_id = ?", (str(user_id),))
rows = cur.fetchall()
conn.close()
total = sum(row[1] for row in rows)
return rows, total
def get_top_swearers(limit: int = 10, guild_id: int | None = None, offset: int = 0):
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
if guild_id:
cur.execute(
"""SELECT user_id, count
FROM swear_counts
WHERE guild_id = ?
ORDER BY count DESC
LIMIT ? OFFSET ?""",
(str(guild_id), limit, offset)
)
else:
cur.execute(
"""SELECT user_id, SUM(count) AS total
FROM swear_counts
GROUP BY user_id
ORDER BY total DESC
LIMIT ? OFFSET ?""",
(limit, offset)
)
results = cur.fetchall()
conn.close()
return results
def get_global_swear_count() -> int:
"""Return total swears across all users and servers."""
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
cur.execute("SELECT SUM(count) FROM swear_counts")
result = cur.fetchone()
conn.close()
return result[0] or 0
# ==============================
# PRIVACY
# ==============================
def is_user_opted_out(user_id: int) -> bool:
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
cur.execute("SELECT 1 FROM privacy_optouts WHERE user_id = ?", (str(user_id),))
result = cur.fetchone()
conn.close()
return result is not None
def set_user_optout(user_id: int):
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
cur.execute("DELETE FROM swear_counts WHERE user_id = ?", (str(user_id),))
cur.execute("INSERT OR IGNORE INTO privacy_optouts (user_id) VALUES (?)", (str(user_id),))
conn.commit()
conn.close()
def remove_user_optout(user_id: int):
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
cur.execute("DELETE FROM privacy_optouts WHERE user_id = ?", (str(user_id),))
conn.commit()
conn.close()
# ==============================
# UTILS
# ==============================
def censor_name(name: str) -> str:
if len(name) <= 3:
return "*" * len(name)
return "*" * (len(name) - 3) + name[-3:]
# ==============================
# MESSAGE TRACKING
# ==============================
@bot.event
async def on_message(message: discord.Message):
if message.author.bot or not message.guild:
return
if is_user_opted_out(message.author.id):
return
content = message.content.lower()
if any(word in content for word in SWEAR_WORDS):
add_swear(message.guild.id, message.author.id)
await bot.process_commands(message)
# ==============================
# COMMANDS
# ==============================
@bot.tree.command(name="swearjar", description="View your or another user's swear count")
async def swearjar(interaction: discord.Interaction, user: discord.User | None = None):
user = user or interaction.user
if is_user_opted_out(user.id):
await interaction.response.send_message(
f"{user.display_name} has opted out of tracking 🔒", ephemeral=True
)
return
rows, total = get_swear_counts(user.id)
if not rows:
await interaction.response.send_message(
f"{user.display_name} hasn't been caught swearing yet 🎉", ephemeral=True
)
return
embed = discord.Embed(title=f"🪣 Swear Jar — {user.display_name}", color=discord.Color.blurple())
for guild_id, count in rows:
g = bot.get_guild(int(guild_id))
embed.add_field(
name=g.name if g else f"Unknown Server ({guild_id})", value=f"{count:,}", inline=False
)
embed.add_field(name="Total across all servers", value=f"**{total:,}**", inline=False)
await interaction.response.send_message(embed=embed)
# ==============================
# LEADERBOARD
# ==============================
class LeaderboardView(discord.ui.View):
def __init__(self, mode: str):
super().__init__(timeout=60)
self.mode = mode
self.page = 0
self.page_size = 10
async def update_page(self, interaction: discord.Interaction):
offset = self.page * self.page_size
guild_id = interaction.guild.id if self.mode == "guild" else None
results = get_top_swearers(limit=self.page_size, guild_id=guild_id, offset=offset)
embed = format_leaderboard(results, self.mode, self.page)
await interaction.response.edit_message(embed=embed, view=self)
@discord.ui.button(label="⬅️ Prev", style=discord.ButtonStyle.primary, disabled=True)
async def prev_page(self, interaction: discord.Interaction, button: discord.ui.Button):
if self.page > 0:
self.page -= 1
button.disabled = self.page == 0
await self.update_page(interaction)
@discord.ui.button(label="➡️ Next", style=discord.ButtonStyle.primary)
async def next_page(self, interaction: discord.Interaction, button: discord.ui.Button):
self.page += 1
await self.update_page(interaction)
def format_leaderboard(results, mode="guild", page=0, per_page=10):
if not results:
return discord.Embed(title="No data yet!", color=discord.Color.gold())
title = "🌍 Global Swear Leaderboard" if mode == "global" else "🏠 Server Swear Leaderboard"
embed = discord.Embed(title=title, color=discord.Color.gold())
desc = ""
for idx, (user_id, count) in enumerate(results, start=1 + page * per_page):
if is_user_opted_out(user_id):
continue
user = bot.get_user(int(user_id))
name = censor_name(user.name if user else "Unknown")
desc += f"**#{idx}** — {name}: `{count:,}`\n"
embed.description = desc or "No data to show."
embed.set_footer(text=f"Page {page+1}")
return embed
@bot.tree.command(name="sweartop", description="Show the top swearers")
async def sweartop(interaction: discord.Interaction, mode: str = "server"):
guild_id = interaction.guild.id if mode.lower() != "global" and interaction.guild else None
results = get_top_swearers(limit=10, guild_id=guild_id)
if not results:
await interaction.response.send_message("No swearing detected yet!", ephemeral=True)
return
embed = format_leaderboard(results, "guild" if guild_id else "global")
view = LeaderboardView("guild" if guild_id else "global")
await interaction.response.send_message(embed=embed, view=view)
# ==============================
# PRIVACY COMMAND
# ==============================
@bot.tree.command(name="privacy", description="View or manage your data privacy settings")
@app_commands.choices(action=[
app_commands.Choice(name="view", value="view"),
app_commands.Choice(name="delete", value="delete"),
app_commands.Choice(name="reenable", value="reenable"),
])
async def privacy(interaction: discord.Interaction, action: app_commands.Choice[str]):
user = interaction.user
if action.value == "view":
opted = is_user_opted_out(user.id)
msg = ("🔒 You are **OPTED OUT** and no data is being tracked."
if opted else
"✅ You are **OPTED IN** and swear counts are being tracked.")
await interaction.response.send_message(msg, ephemeral=True)
elif action.value == "delete":
set_user_optout(user.id)
await interaction.response.send_message(
"✅ Your swear data has been deleted and tracking disabled.", ephemeral=True
)
elif action.value == "reenable":
remove_user_optout(user.id)
await interaction.response.send_message(
"🔓 Tracking has been reenabled. Welcome back to the jar!", ephemeral=True
)
# ==============================
# HELP MENU
# ==============================
@bot.tree.command(name="help", description="Learn how to use the SwearJar bot")
async def help_command(interaction: discord.Interaction):
embed = discord.Embed(
title="🪣 SwearJar Help Menu",
description="Here's what I can do:",
color=discord.Color.blurple()
)
embed.add_field(name="💬 `/swearjar [user]`", value="See your swear totals.", inline=False)
embed.add_field(name="📊 `/sweartop [server/global]`", value="See the top swearers.", inline=False)
embed.add_field(name="🛡️ `/privacy`", value="Control your data and privacy.", inline=False)
embed.add_field(name="🤖 Tracking", value="Automatically tracks certain words (except opted-out users).", inline=False)
embed.set_footer(
text="SwearJar Bot • Be nice, or pay the jar 💰",
icon_url=bot.user.display_avatar.url if bot.user.avatar else None
)
await interaction.response.send_message(embed=embed, ephemeral=True)
# ==============================
# RUN
# ==============================
bot.run(BOT_TOKEN)