Skip to content

Commit 8708d1c

Browse files
authored
Merge branch 'main' into yc45
2 parents 51824b3 + 6688e96 commit 8708d1c

10 files changed

Lines changed: 430 additions & 415 deletions

File tree

README.md

Lines changed: 90 additions & 403 deletions
Large diffs are not rendered by default.

bot.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ async def setup_hook(self) -> None:
104104
'cogs.tod',
105105
'cogs.daily_quests',
106106
'cogs.staff_applications',
107-
'cogs.tts'
107+
'cogs.tts',
108+
'cogs.chowkidar'
108109
]
109110

110111
for ext in feature_cogs:

cogs/chowkidar.py

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import discord
2+
from discord.ext import commands
3+
import aiosqlite
4+
from utils.helpers import EmbedBuilder
5+
6+
def is_staff():
7+
async def predicate(ctx):
8+
if ctx.author.id == ctx.bot.config.owner_id:
9+
return True
10+
return ctx.author.guild_permissions.view_audit_log
11+
return commands.check(predicate)
12+
13+
class Chowkidar(commands.Cog):
14+
def __init__(self, bot):
15+
self.bot = bot
16+
self.watched_users = set()
17+
self.log_channel_id = None
18+
19+
async def cog_load(self):
20+
async with aiosqlite.connect("botdata.db") as db:
21+
await db.execute("CREATE TABLE IF NOT EXISTS chowkidar_config (guild_id INTEGER PRIMARY KEY, channel_id INTEGER)")
22+
await db.execute("CREATE TABLE IF NOT EXISTS chowkidar_tracked (user_id INTEGER PRIMARY KEY)")
23+
await db.commit()
24+
25+
async with db.execute("SELECT channel_id FROM chowkidar_config LIMIT 1") as cursor:
26+
row = await cursor.fetchone()
27+
if row:
28+
self.log_channel_id = row[0]
29+
30+
async with db.execute("SELECT user_id FROM chowkidar_tracked") as cursor:
31+
rows = await cursor.fetchall()
32+
self.watched_users = {row[0] for row in rows}
33+
34+
async def send_log(self, embed: discord.Embed):
35+
if not self.log_channel_id:
36+
return
37+
channel = self.bot.get_channel(self.log_channel_id)
38+
if channel:
39+
await channel.send(embed=embed)
40+
41+
@commands.hybrid_command(name="setwlchannel", description="Set the current channel as the watchlog channel.")
42+
@is_staff()
43+
async def setwlchannel(self, ctx):
44+
if not isinstance(ctx.channel, discord.TextChannel):
45+
await ctx.send(embed=EmbedBuilder.error_embed("Invalid Channel", "This command can only be used in a standard text channel."))
46+
return
47+
48+
self.log_channel_id = ctx.channel.id
49+
async with aiosqlite.connect("botdata.db") as db:
50+
await db.execute("INSERT OR REPLACE INTO chowkidar_config (guild_id, channel_id) VALUES (?, ?)", (ctx.guild.id, ctx.channel.id))
51+
await db.commit()
52+
53+
await ctx.send(embed=EmbedBuilder.success_embed("Channel Configured", f"Watchlog channel has been set to {ctx.channel.mention}."))
54+
55+
@commands.hybrid_command(name="chowkidar", description="Start tracking a user.")
56+
@is_staff()
57+
async def chowkidar(self, ctx, user: discord.Member):
58+
if user.id == self.bot.user.id:
59+
await ctx.send(embed=EmbedBuilder.error_embed("Invalid Target", "The bot cannot be tracked."))
60+
return
61+
62+
if user.guild_permissions.view_audit_log and ctx.author.id != ctx.guild.owner_id:
63+
await ctx.send(embed=EmbedBuilder.error_embed("Invalid Target", "You cannot track another staff member."))
64+
return
65+
66+
self.watched_users.add(user.id)
67+
async with aiosqlite.connect("botdata.db") as db:
68+
await db.execute("INSERT OR IGNORE INTO chowkidar_tracked (user_id) VALUES (?)", (user.id,))
69+
await db.commit()
70+
71+
await ctx.send(embed=EmbedBuilder.success_embed("Tracking Initiated", f"Now tracking actions for {user.mention}."))
72+
73+
@commands.hybrid_command(name="endwl", description="Stop tracking a user.")
74+
@is_staff()
75+
async def endwl(self, ctx, user: discord.Member):
76+
self.watched_users.discard(user.id)
77+
async with aiosqlite.connect("botdata.db") as db:
78+
await db.execute("DELETE FROM chowkidar_tracked WHERE user_id = ?", (user.id,))
79+
await db.commit()
80+
81+
await ctx.send(embed=EmbedBuilder.success_embed("Tracking Terminated", f"Stopped tracking {user.mention}."))
82+
83+
@commands.hybrid_command(name="purgewl", description="Delete all watchlogs for a specific user.")
84+
@is_staff()
85+
async def purgewl(self, ctx, user: discord.Member):
86+
if not self.log_channel_id:
87+
await ctx.send(embed=EmbedBuilder.error_embed("Configuration Error", "Watchlog channel is not set."))
88+
return
89+
90+
log_channel = self.bot.get_channel(self.log_channel_id)
91+
if not log_channel:
92+
await ctx.send(embed=EmbedBuilder.error_embed("Configuration Error", "Watchlog channel could not be found."))
93+
return
94+
95+
await ctx.defer()
96+
to_delete = []
97+
98+
async for msg in log_channel.history(limit=1000):
99+
if msg.author == self.bot.user and msg.embeds:
100+
embed = msg.embeds[0]
101+
if embed.footer and embed.footer.text and f"ID: {user.id}" in embed.footer.text:
102+
to_delete.append(msg)
103+
104+
if to_delete:
105+
for i in range(0, len(to_delete), 100):
106+
await log_channel.delete_messages(to_delete[i:i+100])
107+
108+
await ctx.send(embed=EmbedBuilder.success_embed("Purge Complete", f"Deleted {len(to_delete)} log entries for {user.mention}."))
109+
110+
@commands.Cog.listener()
111+
async def on_message(self, message):
112+
if message.author.bot or message.author.id not in self.watched_users:
113+
return
114+
115+
action = "Message Replied" if message.reference else "Message Sent"
116+
embed = discord.Embed(title=action, description=message.content, color=discord.Color.blue(), timestamp=message.created_at)
117+
embed.set_author(name=str(message.author), icon_url=message.author.display_avatar.url)
118+
embed.add_field(name="Channel", value=message.channel.mention)
119+
embed.add_field(name="Message ID", value=str(message.id))
120+
embed.add_field(name="Message Link", value=f"[Jump to Message]({message.jump_url})", inline=False)
121+
embed.set_footer(text=f"User ID: {message.author.id}")
122+
123+
await self.send_log(embed)
124+
125+
@commands.Cog.listener()
126+
async def on_message_edit(self, before, after):
127+
if after.author.bot or after.author.id not in self.watched_users or before.content == after.content:
128+
return
129+
130+
embed = discord.Embed(title="Message Edited", color=discord.Color.yellow(), timestamp=discord.utils.utcnow())
131+
embed.set_author(name=str(after.author), icon_url=after.author.display_avatar.url)
132+
embed.add_field(name="Before", value=before.content or "None", inline=False)
133+
embed.add_field(name="After", value=after.content or "None", inline=False)
134+
embed.add_field(name="Channel", value=after.channel.mention)
135+
embed.add_field(name="Message ID", value=str(after.id))
136+
embed.add_field(name="Message Link", value=f"[Jump to Message]({after.jump_url})", inline=False)
137+
embed.set_footer(text=f"User ID: {after.author.id}")
138+
139+
await self.send_log(embed)
140+
141+
@commands.Cog.listener()
142+
async def on_message_delete(self, message):
143+
if message.author.bot or message.author.id not in self.watched_users:
144+
return
145+
146+
embed = discord.Embed(title="Message Deleted", description=message.content, color=discord.Color.red(), timestamp=discord.utils.utcnow())
147+
embed.set_author(name=str(message.author), icon_url=message.author.display_avatar.url)
148+
embed.add_field(name="Channel", value=message.channel.mention)
149+
embed.add_field(name="Message ID", value=str(message.id))
150+
embed.set_footer(text=f"User ID: {message.author.id}")
151+
152+
await self.send_log(embed)
153+
154+
@commands.Cog.listener()
155+
async def on_voice_state_update(self, member, before, after):
156+
if member.bot or member.id not in self.watched_users:
157+
return
158+
159+
embed = discord.Embed(color=discord.Color.purple(), timestamp=discord.utils.utcnow())
160+
embed.set_author(name=str(member), icon_url=member.display_avatar.url)
161+
embed.set_footer(text=f"User ID: {member.id}")
162+
163+
if before.channel is None and after.channel is not None:
164+
embed.title = "Joined Voice Channel"
165+
embed.add_field(name="Channel", value=after.channel.mention)
166+
elif before.channel is not None and after.channel is None:
167+
embed.title = "Left Voice Channel"
168+
embed.add_field(name="Channel", value=before.channel.mention)
169+
elif before.channel != after.channel:
170+
embed.title = "Moved Voice Channel"
171+
embed.add_field(name="From", value=before.channel.mention)
172+
embed.add_field(name="To", value=after.channel.mention)
173+
else:
174+
return
175+
176+
await self.send_log(embed)
177+
178+
@commands.Cog.listener()
179+
async def on_raw_reaction_add(self, payload):
180+
if payload.user_id not in self.watched_users:
181+
return
182+
183+
guild = self.bot.get_guild(payload.guild_id)
184+
if not guild:
185+
return
186+
187+
member = guild.get_member(payload.user_id)
188+
if not member or member.bot:
189+
return
190+
191+
channel = guild.get_channel(payload.channel_id)
192+
message_link = f"https://discord.com/channels/{payload.guild_id}/{payload.channel_id}/{payload.message_id}"
193+
194+
embed = discord.Embed(title="Reaction Added", color=discord.Color.teal(), timestamp=discord.utils.utcnow())
195+
embed.set_author(name=str(member), icon_url=member.display_avatar.url)
196+
embed.add_field(name="Emoji", value=str(payload.emoji))
197+
embed.add_field(name="Channel", value=channel.mention if channel else str(payload.channel_id))
198+
embed.add_field(name="Message ID", value=str(payload.message_id))
199+
embed.add_field(name="Message Link", value=f"[Jump to Message]({message_link})", inline=False)
200+
embed.set_footer(text=f"User ID: {member.id}")
201+
202+
await self.send_log(embed)
203+
204+
@commands.Cog.listener()
205+
async def on_member_remove(self, member):
206+
if member.id not in self.watched_users:
207+
return
208+
209+
embed = discord.Embed(title="Left Server", color=discord.Color.dark_grey(), timestamp=discord.utils.utcnow())
210+
embed.set_author(name=str(member), icon_url=member.display_avatar.url)
211+
embed.set_footer(text=f"User ID: {member.id}")
212+
213+
await self.send_log(embed)
214+
215+
self.watched_users.discard(member.id)
216+
async with aiosqlite.connect("botdata.db") as db:
217+
await db.execute("DELETE FROM chowkidar_tracked WHERE user_id = ?", (member.id,))
218+
await db.commit()
219+
220+
async def setup(bot):
221+
await bot.add_cog(Chowkidar(bot))
222+

cogs/fun.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class Fun(commands.Cog):
6363

6464
def __init__(self, bot: commands.Bot):
6565
self.bot = bot
66+
self.question_index = 0
6667
self._absolute_template_cache_bytes: Optional[bytes] = None
6768
self._absolute_template_cache_expires_at = 0.0
6869
self._absolute_template_cache_lock = asyncio.Lock()
@@ -153,6 +154,17 @@ def _build_absolute_gif(
153154
result.seek(0)
154155
return result
155156

157+
def get_next_question(self) -> dict[str, str]:
158+
159+
if self.question_index >= len(TRIVIA_QUESTIONS):
160+
random.shuffle(TRIVIA_QUESTIONS)
161+
self.question_index = 0
162+
163+
question = TRIVIA_QUESTIONS[self.question_index]
164+
self.question_index += 1
165+
166+
return question
167+
156168
async def _get_absolute_template_bytes(self) -> bytes:
157169
now = time.monotonic()
158170
if (
@@ -179,6 +191,7 @@ async def _get_absolute_template_bytes(self) -> bytes:
179191
return template_bytes
180192

181193
@commands.hybrid_command(name="fridge", help="Send a fridge image")
194+
@commands.cooldown(1, 15, commands.BucketType.user)
182195
async def fridge(self, ctx: commands.Context):
183196
"""Send a fridge image (simple utility)."""
184197
# Generate an image locally so it always works (no external hotlinking).
@@ -348,10 +361,6 @@ async def choose(self, ctx: commands.Context, *, choices: str):
348361
)
349362
embed.set_footer(text="CodeVerse Bot | Decision Helper")
350363
await ctx.reply(embed=embed, mention_author=False)
351-
352-
@commands.hybrid_command(
353-
name="absolute", help="Put your avatar on the 'absolute cinema' GIF"
354-
)
355364
@app_commands.describe(text="Text to replace 'cinema' with")
356365
async def absolute(self, ctx: commands.Context, *, text: str):
357366

cogs/misc.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Optional, Any, Union
99
from datetime import datetime, timezone, timedelta
1010
import calendar
11+
from better_profanity import profanity
1112

1213
from utils.config import Config
1314

@@ -190,19 +191,19 @@ async def song(self, ctx: commands.Context, user: Optional[discord.Member] = Non
190191
# Song details
191192
embed.add_field(
192193
name="Track",
193-
value=f"**[{spotify_activity.title}]({spotify_activity.track_url})**",
194+
value=f"**[{profanity.censor(spotify_activity.title)}]({spotify_activity.track_url})**",
194195
inline=False
195196
)
196197

197198
embed.add_field(
198199
name="Artist",
199-
value=", ".join(spotify_activity.artists),
200+
value=profanity.censor(", ".join(spotify_activity.artists)),
200201
inline=True
201202
)
202203

203204
embed.add_field(
204205
name="Album",
205-
value=spotify_activity.album,
206+
value=profanity.censor(spotify_activity.album),
206207
inline=True
207208
)
208209

@@ -242,18 +243,18 @@ async def song(self, ctx: commands.Context, user: Optional[discord.Member] = Non
242243

243244
embed.add_field(
244245
name="Activity",
245-
value=f"**{music_activity.name}**",
246+
value=f"**{profanity.censor(music_activity.name)}**",
246247
inline=False
247248
)
248249

249250
# Use getattr to safely access optional attributes
250251
details = getattr(music_activity, 'details', None)
251252
if details:
252-
embed.add_field(name="Details", value=details, inline=False)
253+
embed.add_field(name="Details", value=profanity.censor(details), inline=False)
253254

254255
state = getattr(music_activity, 'state', None)
255256
if state:
256-
embed.add_field(name="State", value=state, inline=False)
257+
embed.add_field(name="State", value=profanity.censor(state), inline=False)
257258

258259
embed.set_footer(text=f"Requested by {ctx.author.display_name}", icon_url=ctx.author.display_avatar.url)
259260
else:

docs/FEATURES.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Full Feature Library
2+
3+
This document contains the complete list of utility, fun, and community modules.
4+
5+
## Starboard System
6+
Automatically highlight high-quality community content.
7+
- `?starboard setup` : Define channel, emoji, and reaction threshold.
8+
- `?starboard cleanup` : Admin tool to remove invalid or deleted entries.
9+
10+
## Tag System
11+
Store and retrieve custom text snippets.
12+
- `?tags create <name> <content>` : Save a reusable snippet.
13+
- `?tag <name>` : Fetch a saved tag.
14+
15+
## Election & Voting
16+
- `?election create <title> <candidates>` : Start a democratic vote.
17+
- Supports **Weighted Voting** based on user roles or tenure.
18+
19+
## Utility & Community
20+
- **AFK**: `?afk [reason]` - Auto-responds to mentions when you're away.
21+
- **Birthdays**: `?setbirthday <DD/MM>` - Automated birthday wishes.
22+
- **Suggestions**: `/suggest <text>` - Creates an embed with voting reactions and a discussion thread.
23+
- **Disboard Tracker**: `/bumplb` - View the leaderboard for users who bump the server.
24+
25+
[← Back to README](../README.md)

docs/GAMES.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Gaming & Engagement Systems
2+
3+
Eigen Bot integrates multiple systems to reward active members and foster friendly competition.
4+
5+
## Counting Game
6+
An anti-grief counting system with highscore tracking.
7+
- **Anti-Double Count**: Prevents users from counting twice in a row (3 warnings = fail).
8+
- **Save Protection**: Uses **Personal Saves** or **Server Saves** to prevent a reset to 0.
9+
- **Commands**: `/setcountingchannel`, `?highscoretable`, `?donateguild` (Donate 1.0 personal save to gain 0.5 server save).
10+
11+
## CodeBuddy Quizzes
12+
Automated coding challenges to test your community's knowledge.
13+
- **Leaderboards**: Track weekly and all-time streaks.
14+
- **Commands**: `/codeweek`, `/codestreak`, `/codeflex` (Generates a stat card).
15+
16+
## Daily Quest System
17+
Earn rewards by completing daily tasks (e.g., answer 5 quizzes + count 5 numbers).
18+
- **Rewards**: Earn **Streak Freezes** (protects CodeBuddy streaks) and **Saves** (protects the Counting Game).
19+
- **Commands**: `?dailyquest`, `?inventory`.
20+
21+
[← Back to README](../README.md)

0 commit comments

Comments
 (0)