Skip to content

Commit bcd24b3

Browse files
Merge pull request #276 from RemainingDelta/dev
v1.9.1
2 parents 8ce0d50 + 3518a3c commit bcd24b3

9 files changed

Lines changed: 242 additions & 30 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: PR Issue Reference Check
2+
3+
on:
4+
pull_request:
5+
branches: [dev]
6+
types: [opened, edited, synchronize, reopened]
7+
8+
jobs:
9+
pr-issue-reference-check:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Check issue number is consistent across branch, title, and body
13+
env:
14+
BRANCH: ${{ github.head_ref }}
15+
PR_TITLE: ${{ github.event.pull_request.title }}
16+
PR_BODY: ${{ github.event.pull_request.body }}
17+
run: |
18+
# Extract leading issue number from branch name
19+
if [[ "$BRANCH" =~ ^([0-9]+)- ]]; then
20+
ISSUE_NUM="${BASH_REMATCH[1]}"
21+
echo "Issue number: $ISSUE_NUM"
22+
else
23+
echo "ERROR: Branch name '$BRANCH' does not start with an issue number (e.g. 258-Bug)."
24+
exit 1
25+
fi
26+
27+
# Assert PR body contains "Closes #<issue_num>" (case-insensitive)
28+
if echo "$PR_BODY" | grep -iqE "closes #${ISSUE_NUM}([^0-9]|$)"; then
29+
echo "PR body contains 'Closes #${ISSUE_NUM}'. OK."
30+
else
31+
echo "ERROR: PR body must contain 'Closes #${ISSUE_NUM}'."
32+
exit 1
33+
fi
34+
35+
# Assert PR title references the issue number as a whole word
36+
if echo "$PR_TITLE" | grep -qwE "${ISSUE_NUM}"; then
37+
echo "PR title references issue #${ISSUE_NUM}. OK."
38+
else
39+
echo "ERROR: PR title must reference issue number ${ISSUE_NUM}."
40+
exit 1
41+
fi
42+
43+
echo "All issue reference checks passed."
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Version Check
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
7+
jobs:
8+
version-check:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
13+
- name: Fetch main branch
14+
run: git fetch origin main
15+
16+
- name: Check version bump
17+
run: |
18+
PR_VERSION=$(grep -m1 '^version' pyproject.toml | sed 's/.*"\(.*\)"/\1/')
19+
MAIN_VERSION=$(git show origin/main:pyproject.toml | grep -m1 '^version' | sed 's/.*"\(.*\)"/\1/')
20+
echo "PR version: $PR_VERSION"
21+
echo "Main version: $MAIN_VERSION"
22+
if [ "$PR_VERSION" = "$MAIN_VERSION" ]; then
23+
echo "::error::Version in pyproject.toml ($PR_VERSION) has not been bumped. Please update the version before merging to main."
24+
exit 1
25+
fi
26+
echo "Version bumped: $MAIN_VERSION -> $PR_VERSION"

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Shiven Ajwaliya
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

database/mongo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ async def add_hacked_user(user_id: str, reason: str = "Compromised Account"):
229229
async def get_hacked_users():
230230
"""Retrieves all currently hacked users."""
231231
cursor = db.hacked_users.find({"status": "hacked"})
232-
return await cursor.to_list(length=100)
232+
return await cursor.to_list(length=None)
233233

234234

235235
async def remove_hacked_user(user_id: str):

features/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
SUPPORT_STAFF_APPS_CATEGORY_ID = 1330243733177897030
3737
SUPPORT_PARTNERSHIP_CATEGORY_ID = 1330243884911169657
3838
REDEMPTION_TICKET_CATEGORY_ID = 1481456156080738346
39+
BOTS_CATEGORY_ID = 767612682357964810
3940
SUPPORT_TRANSCRIPT_LOG_CHANNEL_ID = 1481117793222004746
4041
SUPPORT_STAFF_APPS_INFO_CHANNEL_ID = 1261020578790248459
4142

@@ -212,6 +213,7 @@
212213
SUPPORT_STAFF_APPS_CATEGORY_ID = 1480728684704043098
213214
SUPPORT_PARTNERSHIP_CATEGORY_ID = 1480728776919879721
214215
REDEMPTION_TICKET_CATEGORY_ID = 1481455542236086293
216+
BOTS_CATEGORY_ID = 1506812222704451677
215217
SUPPORT_TRANSCRIPT_LOG_CHANNEL_ID = 1480735066924781711
216218
SUPPORT_STAFF_APPS_INFO_CHANNEL_ID = 1481083131342618754
217219

features/economy.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
# --- CONFIGURATION ---
2828
from features.config import (
2929
ADMIN_ROLE_ID,
30+
BOTS_CATEGORY_ID,
3031
GENERAL_CHANNEL_ID,
3132
EVENT_ANNOUNCEMENTS_CHANNEL_ID,
3233
SHOP_DATA,
@@ -640,6 +641,10 @@ async def on_message(self, message: discord.Message):
640641
if message.content.startswith("!"):
641642
return
642643

644+
# Skip token rewards for messages in the 'BOTS' category
645+
if message.channel.category and message.channel.category.id == BOTS_CATEGORY_ID:
646+
return
647+
643648
user_id = str(message.author.id)
644649
current_timestamp = time.time()
645650
datetime.utcnow().strftime("%Y-%m-%d")
@@ -682,12 +687,12 @@ async def on_message(self, message: discord.Message):
682687
if should_award_tokens:
683688
earned_tokens = random.randint(2, 5)
684689

685-
# Booster Bonus: 7% Chance (Avg 2% increase)
690+
# Booster Bonus: 17.5% Chance (Avg 5% increase)
686691
SERVER_BOOSTER_ROLE_ID = 647685778255642626
687692
if message.guild:
688693
booster_role = message.guild.get_role(SERVER_BOOSTER_ROLE_ID)
689694
if booster_role and booster_role in message.author.roles:
690-
if random.random() < 0.07:
695+
if random.random() < 0.175:
691696
earned_tokens += 1
692697

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

891+
await interaction.response.defer()
892+
886893
try:
887894
if (
888895
not isinstance(REDEMPTION_TICKET_CATEGORY_ID, int)
889896
or REDEMPTION_TICKET_CATEGORY_ID <= 0
890897
):
891-
await interaction.response.send_message(
898+
await interaction.followup.send(
892899
"❌ Redemption category is not configured.",
893900
ephemeral=True,
894901
)
895902
return
896903

897904
category = interaction.guild.get_channel(REDEMPTION_TICKET_CATEGORY_ID)
898905
if not isinstance(category, discord.CategoryChannel):
899-
await interaction.response.send_message(
906+
await interaction.followup.send(
900907
"❌ Configured redemption category channel was not found.",
901908
ephemeral=True,
902909
)
903910
return
904911

912+
balance_before = await get_user_balance(user_id)
913+
905914
await remove_item_token(user_id, item)
906915

907916
tracking_keys = {
@@ -964,17 +973,31 @@ async def redeem(self, interaction: discord.Interaction, item: str):
964973
description=f"A ticket has been created in {ch.mention}.\nPlease provide the following details to redeem your **{item_info['display']}**:\n{instructions}",
965974
color=discord.Color.green(),
966975
)
967-
await interaction.response.send_message(embed=embed)
976+
await interaction.followup.send(embed=embed)
977+
item_price = item_info["price"]
978+
balance_after = balance_before
979+
balance_display_before = balance_before + item_price
968980
ticket_embed = discord.Embed(
969981
title=f"🎫 **{item.title()} Redemption Ticket**",
970982
description=f"{interaction.user.mention}, please provide the following details in this ticket channel:\n\n{instructions}",
971983
color=discord.Color.blue(),
972984
)
985+
ticket_embed.add_field(
986+
name="Item Price", value=f"{item_price:,} R7 tokens", inline=True
987+
)
988+
ticket_embed.add_field(
989+
name="Balance Before",
990+
value=f"{balance_display_before:,} R7 tokens",
991+
inline=True,
992+
)
993+
ticket_embed.add_field(
994+
name="Balance After", value=f"{balance_after:,} R7 tokens", inline=True
995+
)
973996
await ch.send(embed=ticket_embed)
974997

975998
except Exception as e:
976999
await add_item_token(user_id, item, quantity=1)
977-
await interaction.response.send_message(
1000+
await interaction.followup.send(
9781001
f"❌ **Error** Failed to create ticket: {e}", ephemeral=True
9791002
)
9801003

@@ -1324,7 +1347,7 @@ async def economy_help(self, interaction: discord.Interaction):
13241347
"*Requires 5 messages sent since your last `/daily` claim.*\n"
13251348
f"🪂 **Supply Drops:** Random crates appear in {general_ch}! Click the button to claim.\n"
13261349
f"🏆 **Events:** Earn massive token rewards in {event_ch}.\n"
1327-
"🚀 **Booster Bonus:** Server Boosters receive a **2% increase** in coins on average."
1350+
"🚀 **Booster Bonus:** Server Boosters receive a **5% increase** in coins on average."
13281351
)
13291352
earn_embed.add_field(name="📈 Earning Methods", value=earn_text, inline=False)
13301353
earn_embed.set_thumbnail(url=self.bot.user.display_avatar.url)

features/security.py

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from discord import app_commands
33
from discord.ext import commands
44
from datetime import timedelta, datetime
5+
from math import ceil
56
import asyncio
67

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

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

7069
# 5. Global Message Purge
71-
cutoff_date = datetime.utcnow() - timedelta(days=days_to_clean)
70+
cutoff_date = datetime.utcnow() - timedelta(hours=12)
7271
total_deleted = 0
7372
channels_checked = 0
7473

@@ -107,7 +106,7 @@ async def _execute_hacked_action(
107106
embed.add_field(name="Status", value=timeout_status, inline=False)
108107
embed.add_field(
109108
name="Cleanup Stats",
110-
value=f"🗑️ Deleted **{total_deleted} messages** across **{channels_checked} channels** (Past {days_to_clean} days).",
109+
value=f"🗑️ Deleted **{total_deleted} messages** across **{channels_checked} channels** (Past 12 hours).",
111110
inline=False,
112111
)
113112
embed.add_field(
@@ -130,14 +129,11 @@ async def _send_security_logs(self, embed):
130129
name="hacked",
131130
description="MOD/ADMIN: Flag user as hacked, timeout them, and delete messages.",
132131
)
133-
@app_commands.describe(
134-
user="The hacked user", days_to_clean="Days of messages to delete (default 7)"
135-
)
132+
@app_commands.describe(user="The hacked user")
136133
async def hacked_slash(
137134
self,
138135
interaction: discord.Interaction,
139136
user: discord.Member,
140-
days_to_clean: int = 7,
141137
):
142138
if not await self.has_security_permission(interaction):
143139
await interaction.response.send_message(
@@ -147,7 +143,7 @@ async def hacked_slash(
147143

148144
await interaction.response.defer()
149145
result_embed = await self._execute_hacked_action(
150-
interaction.guild, user, interaction.user, days_to_clean
146+
interaction.guild, user, interaction.user
151147
)
152148
await interaction.followup.send(embed=result_embed)
153149

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

162+
if ctx.message.content.strip() != "!hacked":
163+
return
164+
166165
if not ctx.message.reference:
167166
await ctx.send("❌ Reply to a message with `!hacked` to flag that user.")
168167
return
@@ -229,24 +228,71 @@ async def hackedlist(self, interaction: discord.Interaction):
229228
)
230229
return
231230

232-
embed = discord.Embed(
233-
title="🚨 Hacked Users List", color=discord.Color.dark_red()
231+
view = HackedListView(users, interaction.user)
232+
await interaction.followup.send(embed=view.create_embed(), view=view)
233+
234+
235+
class HackedListView(discord.ui.View):
236+
def __init__(self, users: list, author: discord.User):
237+
super().__init__(timeout=300)
238+
self.users = sorted(
239+
users, key=lambda u: u.get("timestamp", datetime.min), reverse=True
234240
)
235-
description_lines = []
236-
for u in users:
241+
self.author = author
242+
self.per_page = 10
243+
self.current_page = 0
244+
self.total_pages = ceil(len(users) / self.per_page)
245+
self.update_buttons()
246+
247+
def create_embed(self) -> discord.Embed:
248+
start = self.current_page * self.per_page
249+
end = start + self.per_page
250+
page_users = self.users[start:end]
251+
252+
entries = []
253+
for u in page_users:
237254
user_id = u["_id"]
238255
reason = u.get("reason", "No reason provided")
239256
time_str = u.get("timestamp", datetime.utcnow()).strftime("%Y-%m-%d")
240-
description_lines.append(
241-
f"<@{user_id}> (`{user_id}`)\n Reason: *{reason}* ({time_str})"
257+
entries.append(
258+
f"<@{user_id}> (`{user_id}`)\nReason: *{reason}* ({time_str})"
242259
)
243260

244-
full_text = "\n".join(description_lines)
245-
if len(full_text) > 4000:
246-
full_text = full_text[:3900] + "..."
261+
embed = discord.Embed(
262+
title="🚨 Hacked Users List",
263+
description="\n\n".join(entries),
264+
color=discord.Color.dark_red(),
265+
)
266+
embed.set_footer(text=f"Page {self.current_page + 1}/{self.total_pages}")
267+
return embed
268+
269+
def update_buttons(self):
270+
self.prev_button.disabled = self.current_page == 0
271+
self.next_button.disabled = self.current_page == self.total_pages - 1
247272

248-
embed.description = full_text
249-
await interaction.followup.send(embed=embed)
273+
@discord.ui.button(
274+
label="◀ Previous", style=discord.ButtonStyle.blurple, disabled=True
275+
)
276+
async def prev_button(
277+
self, interaction: discord.Interaction, button: discord.ui.Button
278+
):
279+
if interaction.user.id != self.author.id:
280+
await interaction.response.defer()
281+
return
282+
self.current_page -= 1
283+
self.update_buttons()
284+
await interaction.response.edit_message(embed=self.create_embed(), view=self)
285+
286+
@discord.ui.button(label="Next ▶", style=discord.ButtonStyle.blurple)
287+
async def next_button(
288+
self, interaction: discord.Interaction, button: discord.ui.Button
289+
):
290+
if interaction.user.id != self.author.id:
291+
await interaction.response.defer()
292+
return
293+
self.current_page += 1
294+
self.update_buttons()
295+
await interaction.response.edit_message(embed=self.create_embed(), view=self)
250296

251297

252298
async def setup(bot):

0 commit comments

Comments
 (0)