Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8e0f2bd
238-Feature add version bump check workflow
RemainingDelta Apr 26, 2026
caead2e
Merge pull request #245 from RemainingDelta/238-Feature
RemainingDelta Apr 26, 2026
f1c7eb4
190-Enhancement paginate /hacked-list with nav buttons and cleaner la…
RemainingDelta Apr 27, 2026
6d08630
Merge pull request #249 from RemainingDelta/190-Enhancement
RemainingDelta Apr 27, 2026
17ac244
254-Enhancement bump version to v1.10.0
RemainingDelta May 12, 2026
d069086
Merge pull request #255 from RemainingDelta/254-Enhancement
RemainingDelta May 12, 2026
c029716
191-Enhancement update hacked message purge to last 1 hour instead of…
RemainingDelta May 12, 2026
366c67f
Merge pull request #256 from RemainingDelta/191-Enhancement
RemainingDelta May 12, 2026
5ad7f37
258-Bug fix !hacked triggering on messages with trailing text
RemainingDelta May 12, 2026
dd34ac8
Merge pull request #259 from RemainingDelta/258-Bug
RemainingDelta May 12, 2026
3888d60
260-Feature add PR issue reference check workflow
RemainingDelta May 12, 2026
5b72648
Merge pull request #261 from RemainingDelta/260-Feature
RemainingDelta May 12, 2026
c82a529
250-Feature add MIT License
RemainingDelta May 12, 2026
f34fb65
Merge pull request #262 from RemainingDelta/250-Feature
RemainingDelta May 12, 2026
fca0206
192-Enhancement apply 60s slow mode to general on tourney start/end
RemainingDelta May 12, 2026
f370ee1
192-Enhancement send confirmation messages of slowmode
RemainingDelta May 12, 2026
325aed0
192-Enhancement fix linter errors
RemainingDelta May 12, 2026
ea7a6a1
Merge pull request #263 from RemainingDelta/192-Enhancement
RemainingDelta May 12, 2026
62018c0
195-Enhancement include item price and balance before/after in redeem…
RemainingDelta May 12, 2026
f6df726
195-Enhancement fix linter errors
RemainingDelta May 12, 2026
f528ef1
Merge pull request #264 from RemainingDelta/195-Enhancement
RemainingDelta May 12, 2026
5c65884
194-Enhancement add admin role rename on tourney start and restore on…
RemainingDelta May 14, 2026
ace024d
194-Enhancement add confirmation messages for admin role rename
RemainingDelta May 14, 2026
b40195e
Merge pull request #268 from RemainingDelta/194-Enhancement
RemainingDelta May 14, 2026
b002695
269-Enhancement update server booster token gain from 2% to 5%
RemainingDelta May 14, 2026
b9bedc5
Merge pull request #270 from RemainingDelta/269-Enhancement
RemainingDelta May 14, 2026
d19bc95
257-Enhancement skip token rewards for messages in BOTS category channel
RemainingDelta May 21, 2026
5c2ad51
Merge pull request #272 from RemainingDelta/257-Enhancement
RemainingDelta May 21, 2026
6268fe9
191-Enhancement-12hr update message delete to the last 12 hours
RemainingDelta May 21, 2026
dc3a154
Merge pull request #273 from RemainingDelta/191-Enhancement-12hr
RemainingDelta May 21, 2026
8cf5625
274-Enhancement bump down bot version to v1.9.1
RemainingDelta May 21, 2026
3518a3c
Merge pull request #275 from RemainingDelta/274-Enhancement
RemainingDelta May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/pr-issue-reference-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: PR Issue Reference Check

on:
pull_request:
branches: [dev]
types: [opened, edited, synchronize, reopened]

jobs:
pr-issue-reference-check:
runs-on: ubuntu-latest
steps:
- name: Check issue number is consistent across branch, title, and body
env:
BRANCH: ${{ github.head_ref }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
# Extract leading issue number from branch name
if [[ "$BRANCH" =~ ^([0-9]+)- ]]; then
ISSUE_NUM="${BASH_REMATCH[1]}"
echo "Issue number: $ISSUE_NUM"
else
echo "ERROR: Branch name '$BRANCH' does not start with an issue number (e.g. 258-Bug)."
exit 1
fi

# Assert PR body contains "Closes #<issue_num>" (case-insensitive)
if echo "$PR_BODY" | grep -iqE "closes #${ISSUE_NUM}([^0-9]|$)"; then
echo "PR body contains 'Closes #${ISSUE_NUM}'. OK."
else
echo "ERROR: PR body must contain 'Closes #${ISSUE_NUM}'."
exit 1
fi

# Assert PR title references the issue number as a whole word
if echo "$PR_TITLE" | grep -qwE "${ISSUE_NUM}"; then
echo "PR title references issue #${ISSUE_NUM}. OK."
else
echo "ERROR: PR title must reference issue number ${ISSUE_NUM}."
exit 1
fi

echo "All issue reference checks passed."
26 changes: 26 additions & 0 deletions .github/workflows/version-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Version Check

on:
pull_request:
branches: [main]

jobs:
version-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Fetch main branch
run: git fetch origin main

- name: Check version bump
run: |
PR_VERSION=$(grep -m1 '^version' pyproject.toml | sed 's/.*"\(.*\)"/\1/')
MAIN_VERSION=$(git show origin/main:pyproject.toml | grep -m1 '^version' | sed 's/.*"\(.*\)"/\1/')
echo "PR version: $PR_VERSION"
echo "Main version: $MAIN_VERSION"
if [ "$PR_VERSION" = "$MAIN_VERSION" ]; then
echo "::error::Version in pyproject.toml ($PR_VERSION) has not been bumped. Please update the version before merging to main."
exit 1
fi
echo "Version bumped: $MAIN_VERSION -> $PR_VERSION"
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Shiven Ajwaliya

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
2 changes: 1 addition & 1 deletion database/mongo.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ async def add_hacked_user(user_id: str, reason: str = "Compromised Account"):
async def get_hacked_users():
"""Retrieves all currently hacked users."""
cursor = db.hacked_users.find({"status": "hacked"})
return await cursor.to_list(length=100)
return await cursor.to_list(length=None)


async def remove_hacked_user(user_id: str):
Expand Down
2 changes: 2 additions & 0 deletions features/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
SUPPORT_STAFF_APPS_CATEGORY_ID = 1330243733177897030
SUPPORT_PARTNERSHIP_CATEGORY_ID = 1330243884911169657
REDEMPTION_TICKET_CATEGORY_ID = 1481456156080738346
BOTS_CATEGORY_ID = 767612682357964810
SUPPORT_TRANSCRIPT_LOG_CHANNEL_ID = 1481117793222004746
SUPPORT_STAFF_APPS_INFO_CHANNEL_ID = 1261020578790248459

Expand Down Expand Up @@ -212,6 +213,7 @@
SUPPORT_STAFF_APPS_CATEGORY_ID = 1480728684704043098
SUPPORT_PARTNERSHIP_CATEGORY_ID = 1480728776919879721
REDEMPTION_TICKET_CATEGORY_ID = 1481455542236086293
BOTS_CATEGORY_ID = 1506812222704451677
SUPPORT_TRANSCRIPT_LOG_CHANNEL_ID = 1480735066924781711
SUPPORT_STAFF_APPS_INFO_CHANNEL_ID = 1481083131342618754

Expand Down
37 changes: 30 additions & 7 deletions features/economy.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
# --- CONFIGURATION ---
from features.config import (
ADMIN_ROLE_ID,
BOTS_CATEGORY_ID,
GENERAL_CHANNEL_ID,
EVENT_ANNOUNCEMENTS_CHANNEL_ID,
SHOP_DATA,
Expand Down Expand Up @@ -640,6 +641,10 @@ async def on_message(self, message: discord.Message):
if message.content.startswith("!"):
return

# Skip token rewards for messages in the 'BOTS' category
if message.channel.category and message.channel.category.id == BOTS_CATEGORY_ID:
return

user_id = str(message.author.id)
current_timestamp = time.time()
datetime.utcnow().strftime("%Y-%m-%d")
Expand Down Expand Up @@ -682,12 +687,12 @@ async def on_message(self, message: discord.Message):
if should_award_tokens:
earned_tokens = random.randint(2, 5)

# Booster Bonus: 7% Chance (Avg 2% increase)
# Booster Bonus: 17.5% Chance (Avg 5% increase)
SERVER_BOOSTER_ROLE_ID = 647685778255642626
if message.guild:
booster_role = message.guild.get_role(SERVER_BOOSTER_ROLE_ID)
if booster_role and booster_role in message.author.roles:
if random.random() < 0.07:
if random.random() < 0.175:
earned_tokens += 1

current_balance = await get_user_balance(user_id)
Expand Down Expand Up @@ -883,25 +888,29 @@ async def redeem(self, interaction: discord.Interaction, item: str):
await interaction.response.send_message(embed=embed, ephemeral=True)
return

await interaction.response.defer()

try:
if (
not isinstance(REDEMPTION_TICKET_CATEGORY_ID, int)
or REDEMPTION_TICKET_CATEGORY_ID <= 0
):
await interaction.response.send_message(
await interaction.followup.send(
"❌ Redemption category is not configured.",
ephemeral=True,
)
return

category = interaction.guild.get_channel(REDEMPTION_TICKET_CATEGORY_ID)
if not isinstance(category, discord.CategoryChannel):
await interaction.response.send_message(
await interaction.followup.send(
"❌ Configured redemption category channel was not found.",
ephemeral=True,
)
return

balance_before = await get_user_balance(user_id)

await remove_item_token(user_id, item)

tracking_keys = {
Expand Down Expand Up @@ -964,17 +973,31 @@ async def redeem(self, interaction: discord.Interaction, item: str):
description=f"A ticket has been created in {ch.mention}.\nPlease provide the following details to redeem your **{item_info['display']}**:\n{instructions}",
color=discord.Color.green(),
)
await interaction.response.send_message(embed=embed)
await interaction.followup.send(embed=embed)
item_price = item_info["price"]
balance_after = balance_before
balance_display_before = balance_before + item_price
ticket_embed = discord.Embed(
title=f"🎫 **{item.title()} Redemption Ticket**",
description=f"{interaction.user.mention}, please provide the following details in this ticket channel:\n\n{instructions}",
color=discord.Color.blue(),
)
ticket_embed.add_field(
name="Item Price", value=f"{item_price:,} R7 tokens", inline=True
)
ticket_embed.add_field(
name="Balance Before",
value=f"{balance_display_before:,} R7 tokens",
inline=True,
)
ticket_embed.add_field(
name="Balance After", value=f"{balance_after:,} R7 tokens", inline=True
)
await ch.send(embed=ticket_embed)

except Exception as e:
await add_item_token(user_id, item, quantity=1)
await interaction.response.send_message(
await interaction.followup.send(
f"❌ **Error** Failed to create ticket: {e}", ephemeral=True
)

Expand Down Expand Up @@ -1324,7 +1347,7 @@ async def economy_help(self, interaction: discord.Interaction):
"*Requires 5 messages sent since your last `/daily` claim.*\n"
f"🪂 **Supply Drops:** Random crates appear in {general_ch}! Click the button to claim.\n"
f"🏆 **Events:** Earn massive token rewards in {event_ch}.\n"
"🚀 **Booster Bonus:** Server Boosters receive a **2% increase** in coins on average."
"🚀 **Booster Bonus:** Server Boosters receive a **5% increase** in coins on average."
)
earn_embed.add_field(name="📈 Earning Methods", value=earn_text, inline=False)
earn_embed.set_thumbnail(url=self.bot.user.display_avatar.url)
Expand Down
88 changes: 67 additions & 21 deletions features/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from discord import app_commands
from discord.ext import commands
from datetime import timedelta, datetime
from math import ceil
import asyncio

from database.mongo import add_hacked_user, get_hacked_users, remove_hacked_user
Expand All @@ -26,9 +27,7 @@ async def has_security_permission(self, source):
return False

# --- CORE LOGIC: The shared hacked/purge process ---
async def _execute_hacked_action(
self, guild, target_user, moderator, days_to_clean=7
):
async def _execute_hacked_action(self, guild, target_user, moderator):
"""
Shared logic that performs the timeout, DB update, and message purge.
"""
Expand Down Expand Up @@ -68,7 +67,7 @@ async def _execute_hacked_action(
print(f"Failed to DM hacked user {target_user.id}: {e}")

# 5. Global Message Purge
cutoff_date = datetime.utcnow() - timedelta(days=days_to_clean)
cutoff_date = datetime.utcnow() - timedelta(hours=12)
total_deleted = 0
channels_checked = 0

Expand Down Expand Up @@ -107,7 +106,7 @@ async def _execute_hacked_action(
embed.add_field(name="Status", value=timeout_status, inline=False)
embed.add_field(
name="Cleanup Stats",
value=f"🗑️ Deleted **{total_deleted} messages** across **{channels_checked} channels** (Past {days_to_clean} days).",
value=f"🗑️ Deleted **{total_deleted} messages** across **{channels_checked} channels** (Past 12 hours).",
inline=False,
)
embed.add_field(
Expand All @@ -130,14 +129,11 @@ async def _send_security_logs(self, embed):
name="hacked",
description="MOD/ADMIN: Flag user as hacked, timeout them, and delete messages.",
)
@app_commands.describe(
user="The hacked user", days_to_clean="Days of messages to delete (default 7)"
)
@app_commands.describe(user="The hacked user")
async def hacked_slash(
self,
interaction: discord.Interaction,
user: discord.Member,
days_to_clean: int = 7,
):
if not await self.has_security_permission(interaction):
await interaction.response.send_message(
Expand All @@ -147,7 +143,7 @@ async def hacked_slash(

await interaction.response.defer()
result_embed = await self._execute_hacked_action(
interaction.guild, user, interaction.user, days_to_clean
interaction.guild, user, interaction.user
)
await interaction.followup.send(embed=result_embed)

Expand All @@ -163,6 +159,9 @@ async def hacked_text(self, ctx):
if not await self.has_security_permission(ctx):
return

if ctx.message.content.strip() != "!hacked":
return

if not ctx.message.reference:
await ctx.send("❌ Reply to a message with `!hacked` to flag that user.")
return
Expand Down Expand Up @@ -229,24 +228,71 @@ async def hackedlist(self, interaction: discord.Interaction):
)
return

embed = discord.Embed(
title="🚨 Hacked Users List", color=discord.Color.dark_red()
view = HackedListView(users, interaction.user)
await interaction.followup.send(embed=view.create_embed(), view=view)


class HackedListView(discord.ui.View):
def __init__(self, users: list, author: discord.User):
super().__init__(timeout=300)
self.users = sorted(
users, key=lambda u: u.get("timestamp", datetime.min), reverse=True
)
description_lines = []
for u in users:
self.author = author
self.per_page = 10
self.current_page = 0
self.total_pages = ceil(len(users) / self.per_page)
self.update_buttons()

def create_embed(self) -> discord.Embed:
start = self.current_page * self.per_page
end = start + self.per_page
page_users = self.users[start:end]

entries = []
for u in page_users:
user_id = u["_id"]
reason = u.get("reason", "No reason provided")
time_str = u.get("timestamp", datetime.utcnow()).strftime("%Y-%m-%d")
description_lines.append(
f"<@{user_id}> (`{user_id}`)\n Reason: *{reason}* ({time_str})"
entries.append(
f"<@{user_id}> (`{user_id}`)\nReason: *{reason}* ({time_str})"
)

full_text = "\n".join(description_lines)
if len(full_text) > 4000:
full_text = full_text[:3900] + "..."
embed = discord.Embed(
title="🚨 Hacked Users List",
description="\n\n".join(entries),
color=discord.Color.dark_red(),
)
embed.set_footer(text=f"Page {self.current_page + 1}/{self.total_pages}")
return embed

def update_buttons(self):
self.prev_button.disabled = self.current_page == 0
self.next_button.disabled = self.current_page == self.total_pages - 1

embed.description = full_text
await interaction.followup.send(embed=embed)
@discord.ui.button(
label="◀ Previous", style=discord.ButtonStyle.blurple, disabled=True
)
async def prev_button(
self, interaction: discord.Interaction, button: discord.ui.Button
):
if interaction.user.id != self.author.id:
await interaction.response.defer()
return
self.current_page -= 1
self.update_buttons()
await interaction.response.edit_message(embed=self.create_embed(), view=self)

@discord.ui.button(label="Next ▶", style=discord.ButtonStyle.blurple)
async def next_button(
self, interaction: discord.Interaction, button: discord.ui.Button
):
if interaction.user.id != self.author.id:
await interaction.response.defer()
return
self.current_page += 1
self.update_buttons()
await interaction.response.edit_message(embed=self.create_embed(), view=self)


async def setup(bot):
Expand Down
Loading
Loading