Initial commit v2 (token free)
This commit is contained in:
parent
93235081ea
commit
0cd926b9a7
402
app.py
Normal file
402
app.py
Normal file
|
@ -0,0 +1,402 @@
|
||||||
|
import random
|
||||||
|
import discord
|
||||||
|
import asyncio
|
||||||
|
import traceback
|
||||||
|
import random
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
import ast
|
||||||
|
import websockets
|
||||||
|
import json
|
||||||
|
import difflib
|
||||||
|
import io
|
||||||
|
import gzip
|
||||||
|
import re
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
time_regex = re.compile(r'(\d+)([smhd])') # Matches 4m2s, 1h30m, etc.
|
||||||
|
|
||||||
|
def parse_time(time_str):
|
||||||
|
units = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}
|
||||||
|
total_seconds = sum(int(amount) * units[unit] for amount, unit in time_regex.findall(time_str))
|
||||||
|
return total_seconds if total_seconds > 0 else None
|
||||||
|
|
||||||
|
COMMANDS_DIR = "commands"
|
||||||
|
os.makedirs(COMMANDS_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
def load_commands():
|
||||||
|
commands = {}
|
||||||
|
for filename in os.listdir(COMMANDS_DIR):
|
||||||
|
if filename.endswith(".py"):
|
||||||
|
cmd_name = filename[:-3]
|
||||||
|
cmd_path = os.path.join(COMMANDS_DIR, filename)
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location(cmd_name, cmd_path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
if hasattr(module, "run") and callable(module.run):
|
||||||
|
commands[cmd_name] = module.run
|
||||||
|
return commands
|
||||||
|
# Add a tracking file to save channel IDs
|
||||||
|
TRACKED_CHANNELS_FILE = "tracked_channels.json"
|
||||||
|
|
||||||
|
def load_tracked_channels():
|
||||||
|
if os.path.exists(TRACKED_CHANNELS_FILE):
|
||||||
|
with open(TRACKED_CHANNELS_FILE, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def save_tracked_channels(tracked_channels):
|
||||||
|
with open(TRACKED_CHANNELS_FILE, 'w') as f:
|
||||||
|
json.dump(tracked_channels, f)
|
||||||
|
|
||||||
|
async def handle_blacklist(message):
|
||||||
|
if message.author in [696800726084747314]:
|
||||||
|
await message.reply("no")
|
||||||
|
|
||||||
|
class Selfbot(discord.Client):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.default_status = None
|
||||||
|
self.loaded_commands = load_commands()
|
||||||
|
self.tracked_channels = load_tracked_channels()
|
||||||
|
self.last_status = {}
|
||||||
|
self.AFK_STATUS = False # Static variable to track AFK status
|
||||||
|
self.AFK_NOTIFIED_USERS = [] # Static list to store users notified of AFK status
|
||||||
|
self.horsin = []
|
||||||
|
async def on_ready(self):
|
||||||
|
print(f"Logged in as {self.user}")
|
||||||
|
async def handle_afk_dm(self,message):
|
||||||
|
if self.AFK_STATUS and message.author.id not in self.AFK_NOTIFIED_USERS:
|
||||||
|
await message.reply(
|
||||||
|
"Heya, I'm not at my computer right now, if you're requesting something please follow <https://nohello.club>. I'll let you know when I'm back :) \n\n-# This action was automated."
|
||||||
|
)
|
||||||
|
self.AFK_NOTIFIED_USERS.append(message.author.id)
|
||||||
|
async def on_message(self, message):
|
||||||
|
if message.author.id == 1169111190824308768 and "<@1236667927944761396>" in message.content:
|
||||||
|
await message.reply("shut the fuck up")
|
||||||
|
|
||||||
|
if message.content.startswith(".remindme "):
|
||||||
|
await handle_blacklist(message)
|
||||||
|
|
||||||
|
parts = message.content.split(" ", 2)
|
||||||
|
if len(parts) < 3:
|
||||||
|
await message.reply("Usage: `.remindme <time> <message>`", silent=True)
|
||||||
|
else:
|
||||||
|
duration = parse_time(parts[1])
|
||||||
|
if duration is None:
|
||||||
|
await message.reply("Invalid time format. Example: `4m2s`, `1h30m`.", silent=True)
|
||||||
|
else:
|
||||||
|
reminder_text = parts[2]
|
||||||
|
await message.reply(f"Reminder set for {parts[1]}.", silent=True)
|
||||||
|
|
||||||
|
async def reminder_task():
|
||||||
|
await asyncio.sleep(duration)
|
||||||
|
await message.reply(f"{message.author.mention} Reminder: {reminder_text.replace("@", "at")}")
|
||||||
|
|
||||||
|
asyncio.create_task(reminder_task())
|
||||||
|
elif message.content.startswith(".rps "):
|
||||||
|
await handle_blacklist(message)
|
||||||
|
parts = message.content.split(" ",2)
|
||||||
|
if len(parts) != 2:
|
||||||
|
await message.reply("Usage: `.rps <item`",silent=True)
|
||||||
|
item
|
||||||
|
item = parts[1]
|
||||||
|
if item.lower() == "dick":
|
||||||
|
await message.reply("Scissors beats dick any day :3",silent=True) # little easter egg
|
||||||
|
return
|
||||||
|
if item == "<@696800726084747314>":
|
||||||
|
await message.reply("Head so thick that i would try rock but the rock would break")
|
||||||
|
return
|
||||||
|
choice = random.choice([1,2,3])
|
||||||
|
rps_map = dict(zip((1,2,3),[i for i in "rps"]))
|
||||||
|
rps_map_reverse = dict(zip([i for i in "rps"],(1,2,3))) # FIXME: if you cant see the issue you're blind
|
||||||
|
shortmaps = {word[0]: word for word in ["rock", "paper", "scissors"]}
|
||||||
|
|
||||||
|
beat_map = {1:3,2:1,3:2}
|
||||||
|
iid = 0
|
||||||
|
for k,v in rps_map_reverse.items():
|
||||||
|
if item.lower().startswith(k):
|
||||||
|
iid = v
|
||||||
|
break
|
||||||
|
if iid == 0:
|
||||||
|
await message.reply("Invalid choice!",silent=True)
|
||||||
|
return
|
||||||
|
if choice == iid:
|
||||||
|
await message.reply(f"Huh we chose the same thing. Try again!",silent=True)
|
||||||
|
return
|
||||||
|
if beat_map[iid] == choice:
|
||||||
|
await message.reply(f"Welp, ggs, I won. I chose `{shortmaps[rps_map[choice]]}`",silent=True)
|
||||||
|
else:
|
||||||
|
await message.reply(f"Oop, you lost. Try again later! I chose `{shortmaps[rps_map[choice]]}`",silent=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Handle DM if in AFK mode
|
||||||
|
if isinstance(message.channel, discord.DMChannel) and message.author != self.user:
|
||||||
|
await self.handle_afk_dm(message)
|
||||||
|
|
||||||
|
if message.channel.id in self.horsin:
|
||||||
|
await message.add_reaction("🐴")
|
||||||
|
if message.author.id == 1341423498618208257:
|
||||||
|
await message.add_reaction("🪣")
|
||||||
|
if message.author != self.user:
|
||||||
|
if message.author.id == 1351739454141763626:
|
||||||
|
try:
|
||||||
|
await message.delete()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
if message.content.startswith(".horse"):
|
||||||
|
if message.channel.id in self.horsin:
|
||||||
|
self.horsin.remove(message.channel.id)
|
||||||
|
await message.reply("no longer horsin around D:")
|
||||||
|
else:
|
||||||
|
self.horsin.append(message.channel.id)
|
||||||
|
await message.reply(":D")
|
||||||
|
|
||||||
|
if message.content.startswith(".afk"):
|
||||||
|
if not self.AFK_STATUS:
|
||||||
|
self.AFK_STATUS = True
|
||||||
|
await message.reply("k")
|
||||||
|
else:
|
||||||
|
await message.reply("You are already in AFK mode.", silent=True)
|
||||||
|
|
||||||
|
# UNAFK Command
|
||||||
|
elif message.content.startswith(".unafk"):
|
||||||
|
if self.AFK_STATUS:
|
||||||
|
self.AFK_STATUS = False
|
||||||
|
for i in self.AFK_NOTIFIED_USERS:
|
||||||
|
try:
|
||||||
|
user = await self.fetch_user(i) # Fetch user by ID
|
||||||
|
if user:
|
||||||
|
dm_channel = await user.create_dm() # Create DM channel
|
||||||
|
await dm_channel.send("Hey, I'm back, human me will take over now!")
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeWarning from e
|
||||||
|
|
||||||
|
self.AFK_NOTIFIED_USERS.clear() # Clear the AFK notified users list
|
||||||
|
await message.reply("should work")
|
||||||
|
else:
|
||||||
|
await message.reply("You are not in AFK mode.", silent=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if message.content.startswith(".fmt "):
|
||||||
|
try:
|
||||||
|
formated = eval(f"f'{message.content[5:]}'", globals(),locals())
|
||||||
|
print(formated)
|
||||||
|
await asyncio.wait_for(message.edit(formated), timeout=5)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
flashdel = await message.channel.send(f"Error: {e}",silent=True)
|
||||||
|
await message.delete()
|
||||||
|
await flashdel.delete()
|
||||||
|
raise RuntimeWarning from e
|
||||||
|
|
||||||
|
elif message.content.startswith(".eval "):
|
||||||
|
try:
|
||||||
|
formatted = message.content[6:]
|
||||||
|
print(repr(formatted))
|
||||||
|
exec_scope = {
|
||||||
|
"msg": message,
|
||||||
|
"asyncio": asyncio,
|
||||||
|
"random": random,
|
||||||
|
**self.loaded_commands, # Inject loaded commands
|
||||||
|
"out": lambda content: message.reply(content, silent=True),
|
||||||
|
}
|
||||||
|
# Parse the code to detect non-async function calls
|
||||||
|
tree = ast.parse(formatted)
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.Call) and not isinstance(node.func, ast.Attribute):
|
||||||
|
# Check if the function is a coroutine
|
||||||
|
func_name = node.func.id
|
||||||
|
if func_name in exec_scope and asyncio.iscoroutinefunction(exec_scope[func_name]):
|
||||||
|
# Replace the call with an await expression
|
||||||
|
formatted = formatted.replace(f"{func_name}(", f"await {func_name}(")
|
||||||
|
|
||||||
|
|
||||||
|
exec(f"async def __eval():\n {formatted.replace(chr(10), chr(10) + ' ')}", exec_scope)
|
||||||
|
|
||||||
|
result = await exec_scope["__eval"]()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
await message.edit(content=traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
await message.delete()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
elif message.content.startswith(".addcmd "):
|
||||||
|
try:
|
||||||
|
parts = message.content.split(" ", 2)
|
||||||
|
if len(parts) < 3:
|
||||||
|
await message.reply("Usage: .addcmd <name> <code>",silent=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd_name, code = parts[1], parts[2]
|
||||||
|
cmd_path = os.path.join(COMMANDS_DIR, f"{cmd_name}.py")
|
||||||
|
|
||||||
|
with open(cmd_path, "w") as f:
|
||||||
|
f.write("async def run(msg):\n")
|
||||||
|
for line in code.split("\n"):
|
||||||
|
f.write(f" {line}\n")
|
||||||
|
|
||||||
|
self.loaded_commands = load_commands()
|
||||||
|
await message.reply(f"Command {cmd_name} saved.",silent=True)
|
||||||
|
except Exception as e:
|
||||||
|
await message.reply(f"Error: {e}",silent=True)
|
||||||
|
|
||||||
|
elif message.content.startswith(".delcmd "):
|
||||||
|
cmd_name = message.content.split(" ", 1)[1]
|
||||||
|
cmd_path = os.path.join(COMMANDS_DIR, f"{cmd_name}.py")
|
||||||
|
|
||||||
|
if os.path.exists(cmd_path):
|
||||||
|
os.remove(cmd_path)
|
||||||
|
self.loaded_commands = load_commands()
|
||||||
|
await message.reply(f"Command {cmd_name} deleted.",silent=True)
|
||||||
|
else:
|
||||||
|
await message.reply(f"Command {cmd_name} not found.",silent=True)
|
||||||
|
|
||||||
|
elif message.content.startswith(".listcmds"):
|
||||||
|
cmds = list(self.loaded_commands.keys())
|
||||||
|
await message.reply("Saved commands:\n" + ", ".join(cmds) if cmds else "No saved commands.",silent=True)
|
||||||
|
elif message.content.startswith(".delrecent "):
|
||||||
|
try:
|
||||||
|
minutes = float(message.content.split(" ", 1)[1])
|
||||||
|
cutoff_time = datetime.datetime.now(datetime.UTC) - datetime.timedelta(minutes=minutes)
|
||||||
|
|
||||||
|
deleted = 0
|
||||||
|
async for msg in message.channel.history(limit=100):
|
||||||
|
if msg.author == self.user and msg.created_at >= cutoff_time:
|
||||||
|
await msg.delete()
|
||||||
|
deleted += 1
|
||||||
|
|
||||||
|
await message.channel.send(f"Deleted {deleted} messages.", delete_after=0,silent=True)
|
||||||
|
except Exception as e:
|
||||||
|
await message.channel.send(f"Error: {e}", delete_after=0,silent=True)
|
||||||
|
elif message.content.startswith(".trackmessages"):
|
||||||
|
channel_id = message.channel.id
|
||||||
|
if channel_id not in self.tracked_channels:
|
||||||
|
self.tracked_channels.append(channel_id)
|
||||||
|
save_tracked_channels(self.tracked_channels)
|
||||||
|
await message.reply(f"Tracking messages in this channel {message.channel.name}.",silent=True)
|
||||||
|
else:
|
||||||
|
await message.reply("This channel is already being tracked.",silent=True)
|
||||||
|
|
||||||
|
elif message.content.startswith(".untrackmessages"):
|
||||||
|
channel_id = message.channel.id
|
||||||
|
if channel_id in self.tracked_channels:
|
||||||
|
self.tracked_channels.remove(channel_id)
|
||||||
|
save_tracked_channels(self.tracked_channels)
|
||||||
|
await message.reply(f"Stopped tracking messages in {message.channel.name}.",silent=True)
|
||||||
|
else:
|
||||||
|
await message.reply("This channel is not being tracked.",silent=True)
|
||||||
|
elif message.content.startswith(".savechannel"):
|
||||||
|
try:
|
||||||
|
messages = []
|
||||||
|
async for msg in message.channel.history(limit=None):
|
||||||
|
msg_dict = {
|
||||||
|
"id": msg.id,
|
||||||
|
"content": msg.content,
|
||||||
|
"created_at": msg.created_at.isoformat() if msg.created_at else None,
|
||||||
|
"edited_at": msg.edited_at.isoformat() if msg.edited_at else None,
|
||||||
|
"author": {
|
||||||
|
"id": msg.author.id,
|
||||||
|
"name": msg.author.name,
|
||||||
|
"discriminator": msg.author.discriminator,
|
||||||
|
"bot": msg.author.bot
|
||||||
|
} if msg.author else None,
|
||||||
|
"channel_id": msg.channel.id,
|
||||||
|
"attachments": [
|
||||||
|
{"filename": a.filename, "url": a.url} for a in msg.attachments
|
||||||
|
] if msg.attachments else [],
|
||||||
|
"embeds": [embed.to_dict() for embed in msg.embeds] if msg.embeds else [],
|
||||||
|
"reactions": [
|
||||||
|
{"emoji": str(r.emoji), "count": r.count} for r in msg.reactions
|
||||||
|
] if msg.reactions else [],
|
||||||
|
"mentions": [user.id for user in msg.mentions] if msg.mentions else [],
|
||||||
|
"role_mentions": [role.id for role in msg.role_mentions] if msg.role_mentions else [],
|
||||||
|
"pinned": msg.pinned,
|
||||||
|
"tts": msg.tts,
|
||||||
|
"type": str(msg.type),
|
||||||
|
"reference": {
|
||||||
|
"message_id": msg.reference.message_id,
|
||||||
|
"channel_id": msg.reference.channel_id,
|
||||||
|
"guild_id": msg.reference.guild_id
|
||||||
|
} if msg.reference else None,
|
||||||
|
}
|
||||||
|
messages.append(msg_dict)
|
||||||
|
|
||||||
|
filename = f"{message.channel.id}.json.gz"
|
||||||
|
filepath = os.path.join("downloads", filename) # Save to a "downloads" folder
|
||||||
|
os.makedirs("downloads", exist_ok=True)
|
||||||
|
|
||||||
|
with gzip.open(filepath, "wt", encoding="utf-8") as f:
|
||||||
|
json.dump(messages, f, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
await message.edit(content=f"Saved messages to `{filepath}`")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await message.edit(content=f"Error: {e}")
|
||||||
|
elif message.content.startswith(".repeat29"):
|
||||||
|
await message.reply("oop ok")
|
||||||
|
while 1:
|
||||||
|
await message.channel.send("you asked dad")
|
||||||
|
await asyncio.sleep(29*60)
|
||||||
|
@staticmethod
|
||||||
|
async def process_log_whitelist(message):
|
||||||
|
if message.author in [627566973869359104,]:
|
||||||
|
return
|
||||||
|
|
||||||
|
async def on_message_delete(self, message):
|
||||||
|
await self.process_log_whitelist(message)
|
||||||
|
if message.channel.id in self.tracked_channels:
|
||||||
|
member = message.author
|
||||||
|
if member != self.user:
|
||||||
|
await message.channel.send(f"<@{member.id}> deleted {message.content}",silent=True)
|
||||||
|
|
||||||
|
async def on_message_edit(self, before, after):
|
||||||
|
await self.process_log_whitelist(before)
|
||||||
|
if before.channel.id in self.tracked_channels:
|
||||||
|
member = after.author
|
||||||
|
if member == self.user: return
|
||||||
|
if before.content == after.content: return
|
||||||
|
diff = difflib.unified_diff(before.content.splitlines(), after.content.splitlines())
|
||||||
|
diff_result = '\n'.join(diff)
|
||||||
|
|
||||||
|
# Use BytesIO to create an in-memory file-like object with the full diff (no line removal)
|
||||||
|
with io.BytesIO(diff_result.encode('utf-8')) as diff_file:
|
||||||
|
diff_file.seek(0) # Ensure we're at the start of the BytesIO buffer
|
||||||
|
|
||||||
|
# Send the file to the channel
|
||||||
|
await after.channel.send(
|
||||||
|
f"<@{member.id}> edited a message",
|
||||||
|
file=discord.File(diff_file, "cutie.diff"),silent=True
|
||||||
|
)
|
||||||
|
async def on_presence_update(self, before, after):
|
||||||
|
if after.id == 627566973869359104:
|
||||||
|
old_status = self.last_status.get(after.id, discord.Status.offline)
|
||||||
|
self.last_status[after.id] = after.status
|
||||||
|
|
||||||
|
if old_status in [discord.Status.offline] and after.status == discord.Status.online:
|
||||||
|
channel = self.get_channel(1302691935152246851)
|
||||||
|
if channel:
|
||||||
|
...
|
||||||
|
#await channel.send(f"[BOT] Welcome back, {after.mention}!",silent=True)
|
||||||
|
|
||||||
|
client = Selfbot()
|
||||||
|
# get token from environment variable
|
||||||
|
TOKEN = os.getenv("TOKEN")
|
||||||
|
if not TOKEN:
|
||||||
|
raise ValueError("No TOKEN found in environment variables. Please add it to your .env file.")
|
||||||
|
client.run(TOKEN)
|
1
bot/__init__.py
Normal file
1
bot/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Empty init to make directory a package
|
1
bot/commands/__init__.py
Normal file
1
bot/commands/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Empty init to make directory a package
|
65
bot/commands/admin_commands.py
Normal file
65
bot/commands/admin_commands.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import os
|
||||||
|
from config import COMMANDS_DIR
|
||||||
|
from utils.storage import save_tracked_channels
|
||||||
|
|
||||||
|
class AdminCommands:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
async def cmd_addcmd(self, message):
|
||||||
|
"""Add a custom command"""
|
||||||
|
try:
|
||||||
|
parts = message.content.split(" ", 2)
|
||||||
|
if len(parts) < 3:
|
||||||
|
await message.reply("Usage: .addcmd <name> <code>", silent=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd_name, code = parts[1], parts[2]
|
||||||
|
cmd_path = os.path.join(COMMANDS_DIR, f"{cmd_name}.py")
|
||||||
|
|
||||||
|
with open(cmd_path, "w") as f:
|
||||||
|
f.write("async def run(msg):\n")
|
||||||
|
for line in code.split("\n"):
|
||||||
|
f.write(f" {line}\n")
|
||||||
|
|
||||||
|
self.bot.reload_commands()
|
||||||
|
await message.reply(f"Command {cmd_name} saved.", silent=True)
|
||||||
|
except Exception as e:
|
||||||
|
await message.reply(f"Error: {e}", silent=True)
|
||||||
|
|
||||||
|
async def cmd_delcmd(self, message):
|
||||||
|
"""Delete a custom command"""
|
||||||
|
cmd_name = message.content.split(" ", 1)[1]
|
||||||
|
cmd_path = os.path.join(COMMANDS_DIR, f"{cmd_name}.py")
|
||||||
|
|
||||||
|
if os.path.exists(cmd_path):
|
||||||
|
os.remove(cmd_path)
|
||||||
|
self.bot.reload_commands()
|
||||||
|
await message.reply(f"Command {cmd_name} deleted.", silent=True)
|
||||||
|
else:
|
||||||
|
await message.reply(f"Command {cmd_name} not found.", silent=True)
|
||||||
|
|
||||||
|
async def cmd_listcmds(self, message):
|
||||||
|
"""List all custom commands"""
|
||||||
|
cmds = list(self.bot.loaded_commands.keys())
|
||||||
|
await message.reply("Saved commands:\n" + ", ".join(cmds) if cmds else "No saved commands.", silent=True)
|
||||||
|
|
||||||
|
async def cmd_trackmessages(self, message):
|
||||||
|
"""Start tracking messages in the current channel"""
|
||||||
|
channel_id = message.channel.id
|
||||||
|
if channel_id not in self.bot.tracked_channels:
|
||||||
|
self.bot.tracked_channels.append(channel_id)
|
||||||
|
save_tracked_channels(self.bot.tracked_channels)
|
||||||
|
await message.reply(f"Tracking messages in this channel {message.channel.name}.", silent=True)
|
||||||
|
else:
|
||||||
|
await message.reply("This channel is already being tracked.", silent=True)
|
||||||
|
|
||||||
|
async def cmd_untrackmessages(self, message):
|
||||||
|
"""Stop tracking messages in the current channel"""
|
||||||
|
channel_id = message.channel.id
|
||||||
|
if channel_id in self.bot.tracked_channels:
|
||||||
|
self.bot.tracked_channels.remove(channel_id)
|
||||||
|
save_tracked_channels(self.bot.tracked_channels)
|
||||||
|
await message.reply(f"Stopped tracking messages in {message.channel.name}.", silent=True)
|
||||||
|
else:
|
||||||
|
await message.reply("This channel is not being tracked.", silent=True)
|
38
bot/commands/afk_commands.py
Normal file
38
bot/commands/afk_commands.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
class AfkCommands:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
async def handle_afk_dm(self, message):
|
||||||
|
"""Handle DMs when in AFK mode"""
|
||||||
|
if self.bot.AFK_STATUS and message.author.id not in self.bot.AFK_NOTIFIED_USERS:
|
||||||
|
await message.reply(
|
||||||
|
"Heya, I'm not at my computer right now, if you're requesting something please follow <https://nohello.club>. I'll let you know when I'm back :) \n\n-# This action was automated."
|
||||||
|
)
|
||||||
|
self.bot.AFK_NOTIFIED_USERS.append(message.author.id)
|
||||||
|
|
||||||
|
async def cmd_afk(self, message):
|
||||||
|
"""Enable AFK mode"""
|
||||||
|
if not self.bot.AFK_STATUS:
|
||||||
|
self.bot.AFK_STATUS = True
|
||||||
|
await message.reply("k")
|
||||||
|
else:
|
||||||
|
await message.reply("You are already in AFK mode.", silent=True)
|
||||||
|
|
||||||
|
async def cmd_unafk(self, message):
|
||||||
|
"""Disable AFK mode"""
|
||||||
|
if self.bot.AFK_STATUS:
|
||||||
|
self.bot.AFK_STATUS = False
|
||||||
|
|
||||||
|
for i in self.bot.AFK_NOTIFIED_USERS:
|
||||||
|
try:
|
||||||
|
user = await self.bot.fetch_user(i)
|
||||||
|
if user:
|
||||||
|
dm_channel = await user.create_dm()
|
||||||
|
await dm_channel.send("Hey, I'm back, human me will take over now!")
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeWarning from e
|
||||||
|
|
||||||
|
self.bot.AFK_NOTIFIED_USERS.clear()
|
||||||
|
await message.reply("should work")
|
||||||
|
else:
|
||||||
|
await message.reply("You are not in AFK mode.", silent=True)
|
65
bot/commands/fun_commands.py
Normal file
65
bot/commands/fun_commands.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import random
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
class FunCommands:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
async def cmd_horse(self, message):
|
||||||
|
"""Toggle horse reactions in a channel"""
|
||||||
|
if message.channel.id in self.bot.horsin:
|
||||||
|
self.bot.horsin.remove(message.channel.id)
|
||||||
|
await message.reply("no longer horsin around D:")
|
||||||
|
else:
|
||||||
|
self.bot.horsin.append(message.channel.id)
|
||||||
|
await message.reply(":D")
|
||||||
|
|
||||||
|
async def cmd_rps(self, message):
|
||||||
|
"""Play rock, paper, scissors"""
|
||||||
|
parts = message.content.split(" ", 1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
await message.reply("Usage: `.rps <item>`", silent=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
item = parts[1]
|
||||||
|
|
||||||
|
# Easter eggs
|
||||||
|
if item.lower() == "dick":
|
||||||
|
await message.reply("Scissors beats dick any day :3", silent=True)
|
||||||
|
return
|
||||||
|
if item == "<@696800726084747314>":
|
||||||
|
await message.reply("Head so thick that i would try rock but the rock would break")
|
||||||
|
return
|
||||||
|
|
||||||
|
# The main game
|
||||||
|
choice = random.choice([1, 2, 3]) # 1=rock, 2=paper, 3=scissors
|
||||||
|
rps_map = {1: 'r', 2: 'p', 3: 's'}
|
||||||
|
rps_map_reverse = {'r': 1, 'p': 2, 's': 3} # Fixed the reversed mapping
|
||||||
|
shortmaps = {'r': 'rock', 'p': 'paper', 's': 'scissors'}
|
||||||
|
|
||||||
|
beat_map = {1: 3, 2: 1, 3: 2} # What beats what: rock>scissors, paper>rock, scissors>paper
|
||||||
|
|
||||||
|
# Determine user choice
|
||||||
|
iid = 0
|
||||||
|
for k, v in rps_map_reverse.items():
|
||||||
|
if item.lower().startswith(k):
|
||||||
|
iid = v
|
||||||
|
break
|
||||||
|
|
||||||
|
if iid == 0:
|
||||||
|
await message.reply("Invalid choice!", silent=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if choice == iid:
|
||||||
|
await message.reply(f"Huh we chose the same thing. Try again!", silent=True)
|
||||||
|
elif beat_map[iid] == choice:
|
||||||
|
await message.reply(f"Welp, ggs, I won. I chose `{shortmaps[rps_map[choice]]}`", silent=True)
|
||||||
|
else:
|
||||||
|
await message.reply(f"Oop, you lost. Try again later! I chose `{shortmaps[rps_map[choice]]}`", silent=True)
|
||||||
|
|
||||||
|
async def cmd_repeat(self, message):
|
||||||
|
"""Repeat a message every 29 minutes"""
|
||||||
|
await message.reply("oop ok")
|
||||||
|
while True:
|
||||||
|
await message.channel.send("you asked dad")
|
||||||
|
await asyncio.sleep(29 * 60) # 29 minutes
|
430
bot/commands/test_commands.py
Normal file
430
bot/commands/test_commands.py
Normal file
|
@ -0,0 +1,430 @@
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
import inspect
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class TestCommands:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.tests = {
|
||||||
|
"afk": self.test_afk,
|
||||||
|
"remindme": self.test_remindme,
|
||||||
|
"commands": self.test_commands,
|
||||||
|
"storage": self.test_storage,
|
||||||
|
"health": self.test_health,
|
||||||
|
"all": self.test_all,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def cmd_test(self, message):
|
||||||
|
"""Run self-tests on the bot"""
|
||||||
|
parts = message.content.split(" ", 1)
|
||||||
|
test_name = parts[1] if len(parts) > 1 else "all"
|
||||||
|
|
||||||
|
if test_name not in self.tests:
|
||||||
|
await message.channel.send(
|
||||||
|
f"Unknown test '{test_name}'. Available tests: {', '.join(self.tests.keys())}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create a status message without silent flag for better visibility
|
||||||
|
status_msg = await message.channel.send(f"🔄 Running test: {test_name}...")
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if test_name == "all":
|
||||||
|
# For "all" tests, update status more frequently
|
||||||
|
await status_msg.edit(content="🔄 Initializing test suite...")
|
||||||
|
results = await self.test_all(status_msg)
|
||||||
|
else:
|
||||||
|
test_func = self.tests[test_name]
|
||||||
|
results = await test_func(status_msg)
|
||||||
|
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
|
||||||
|
# Format results into a nice report
|
||||||
|
report = self._format_test_report(results, elapsed)
|
||||||
|
|
||||||
|
# Make sure report isn't too long for Discord
|
||||||
|
if len(report) > 2000:
|
||||||
|
report = report[:1997] + "..."
|
||||||
|
|
||||||
|
# Update status with results
|
||||||
|
await status_msg.edit(content=report)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"❌ Test failed with error:\n\n{traceback.format_exc()[:1500]}\n"
|
||||||
|
print(f"Test error: {str(e)}")
|
||||||
|
await status_msg.edit(content=error_msg)
|
||||||
|
|
||||||
|
def _format_test_report(self, results, elapsed):
|
||||||
|
"""Format test results into a readable report"""
|
||||||
|
if isinstance(results, dict):
|
||||||
|
# We have multiple test suites
|
||||||
|
total_passed = sum(r['passed'] for r in results.values())
|
||||||
|
total_failed = sum(r['failed'] for r in results.values())
|
||||||
|
total_tests = total_passed + total_failed
|
||||||
|
|
||||||
|
report = f"# Self-Test Report ({elapsed:.2f}s)\n\n"
|
||||||
|
report += f"✅ **{total_passed}/{total_tests}** tests passed\n"
|
||||||
|
|
||||||
|
if total_failed > 0:
|
||||||
|
report += f"❌ **{total_failed}** tests failed\n\n"
|
||||||
|
else:
|
||||||
|
report += "\n"
|
||||||
|
|
||||||
|
# Add individual test suite results
|
||||||
|
for suite_name, suite_result in results.items():
|
||||||
|
passed = suite_result['passed']
|
||||||
|
failed = suite_result['failed']
|
||||||
|
total = passed + failed
|
||||||
|
status = "✅" if failed == 0 else "⚠️"
|
||||||
|
|
||||||
|
report += f"{status} **{suite_name}**: {passed}/{total} passed\n"
|
||||||
|
|
||||||
|
# Add failure details if any
|
||||||
|
if failed > 0 and 'failures' in suite_result:
|
||||||
|
report += "\n"
|
||||||
|
for failure in suite_result['failures']:
|
||||||
|
report += f" ❌ {failure}\n"
|
||||||
|
report += "\n"
|
||||||
|
else:
|
||||||
|
# Single test suite
|
||||||
|
passed = results['passed']
|
||||||
|
failed = results['failed']
|
||||||
|
total = passed + failed
|
||||||
|
|
||||||
|
report = f"# Test Results ({elapsed:.2f}s)\n\n"
|
||||||
|
report += f"✅ **{passed}/{total}** tests passed\n"
|
||||||
|
|
||||||
|
if failed > 0:
|
||||||
|
report += f"❌ **{failed}** tests failed\n\n"
|
||||||
|
report += "\n"
|
||||||
|
for failure in results.get('failures', []):
|
||||||
|
report += f" ❌ {failure}\n"
|
||||||
|
report += "\n"
|
||||||
|
|
||||||
|
return report
|
||||||
|
|
||||||
|
async def test_all(self, status_msg):
|
||||||
|
"""Run all available tests"""
|
||||||
|
results = {}
|
||||||
|
test_funcs = [v for k, v in self.tests.items() if k != "all"]
|
||||||
|
|
||||||
|
for i, test_func in enumerate(test_funcs):
|
||||||
|
test_name = test_func.__name__.replace('test_', '')
|
||||||
|
try:
|
||||||
|
# Update status before each test
|
||||||
|
await status_msg.edit(content=f"🔄 Running tests ({i+1}/{len(test_funcs)}): {test_name}...")
|
||||||
|
results[test_name] = await test_func(status_msg)
|
||||||
|
# Quick status after each test
|
||||||
|
passed = results[test_name]['passed']
|
||||||
|
failed = results[test_name]['failed']
|
||||||
|
await status_msg.edit(content=f"🔄 Test {test_name}: ✅{passed} ❌{failed} | Continuing tests...")
|
||||||
|
except Exception as e:
|
||||||
|
results[test_name] = {'passed': 0, 'failed': 1, 'failures': [f"Exception: {str(e)}"]}
|
||||||
|
await status_msg.edit(content=f"⚠️ Error in test {test_name}, continuing with next test...")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
async def test_afk(self, status_msg):
|
||||||
|
"""Test AFK functionality"""
|
||||||
|
results = {'passed': 0, 'failed': 0, 'failures': []}
|
||||||
|
|
||||||
|
# Save original state
|
||||||
|
original_afk = self.bot.AFK_STATUS
|
||||||
|
original_notified = self.bot.AFK_NOTIFIED_USERS.copy()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test 1: Enable AFK
|
||||||
|
self.bot.AFK_STATUS = False
|
||||||
|
await self.bot.afk_commands.cmd_afk(status_msg)
|
||||||
|
|
||||||
|
if self.bot.AFK_STATUS:
|
||||||
|
results['passed'] += 1
|
||||||
|
else:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append("Failed to enable AFK mode")
|
||||||
|
|
||||||
|
# Test 2: Disable AFK
|
||||||
|
await self.bot.afk_commands.cmd_unafk(status_msg)
|
||||||
|
|
||||||
|
if not self.bot.AFK_STATUS:
|
||||||
|
results['passed'] += 1
|
||||||
|
else:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append("Failed to disable AFK mode")
|
||||||
|
|
||||||
|
# Test 3: AFK notification list clears
|
||||||
|
if len(self.bot.AFK_NOTIFIED_USERS) == 0:
|
||||||
|
results['passed'] += 1
|
||||||
|
else:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append("AFK notified users list not cleared")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Restore original state
|
||||||
|
self.bot.AFK_STATUS = original_afk
|
||||||
|
self.bot.AFK_NOTIFIED_USERS = original_notified
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
async def test_remindme(self, status_msg):
|
||||||
|
"""Test reminder functionality with a very short reminder"""
|
||||||
|
results = {'passed': 0, 'failed': 0, 'failures': []}
|
||||||
|
|
||||||
|
# Create a test reminder message
|
||||||
|
test_content = ".remindme 1s Test reminder"
|
||||||
|
mock_message = MockMessage(
|
||||||
|
author=self.bot.user,
|
||||||
|
content=test_content,
|
||||||
|
channel=status_msg.channel
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up a flag to verify the reminder was triggered
|
||||||
|
reminder_triggered = False
|
||||||
|
original_reply = mock_message.reply
|
||||||
|
|
||||||
|
async def mock_reply(content, **kwargs):
|
||||||
|
nonlocal reminder_triggered
|
||||||
|
if "Reminder:" in content:
|
||||||
|
reminder_triggered = True
|
||||||
|
return await original_reply(content, **kwargs)
|
||||||
|
|
||||||
|
mock_message.reply = mock_reply
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test reminder setup
|
||||||
|
await self.bot.utility_commands.cmd_remindme(mock_message)
|
||||||
|
|
||||||
|
# Wait for the reminder to trigger (slightly more than 1s)
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
|
||||||
|
if reminder_triggered:
|
||||||
|
results['passed'] += 1
|
||||||
|
else:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append("Reminder did not trigger")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append(f"Reminder test error: {str(e)}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
async def test_commands(self, status_msg):
|
||||||
|
"""Test custom command functionality"""
|
||||||
|
results = {'passed': 0, 'failed': 0, 'failures': []}
|
||||||
|
|
||||||
|
# Test command name
|
||||||
|
test_cmd_name = "__test_cmd__"
|
||||||
|
test_cmd_path = f"commands/{test_cmd_name}.py"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test 1: Add a command
|
||||||
|
add_cmd_msg = MockMessage(
|
||||||
|
author=self.bot.user,
|
||||||
|
content=f".addcmd {test_cmd_name} return await msg.reply('test success', silent=True)",
|
||||||
|
channel=status_msg.channel
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.bot.admin_commands.cmd_addcmd(add_cmd_msg)
|
||||||
|
|
||||||
|
if test_cmd_name in self.bot.loaded_commands:
|
||||||
|
results['passed'] += 1
|
||||||
|
else:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append("Failed to add test command")
|
||||||
|
|
||||||
|
# Test 2: Execute the command
|
||||||
|
if test_cmd_name in self.bot.loaded_commands:
|
||||||
|
try:
|
||||||
|
test_msg = MockMessage(
|
||||||
|
author=self.bot.user,
|
||||||
|
content="test content",
|
||||||
|
channel=status_msg.channel
|
||||||
|
)
|
||||||
|
|
||||||
|
reply_received = False
|
||||||
|
original_reply = test_msg.reply
|
||||||
|
|
||||||
|
async def test_reply(content, **kwargs):
|
||||||
|
nonlocal reply_received
|
||||||
|
if content == "test success":
|
||||||
|
reply_received = True
|
||||||
|
return await original_reply(content, **kwargs)
|
||||||
|
|
||||||
|
test_msg.reply = test_reply
|
||||||
|
|
||||||
|
await self.bot.loaded_commands[test_cmd_name](test_msg)
|
||||||
|
|
||||||
|
if reply_received:
|
||||||
|
results['passed'] += 1
|
||||||
|
else:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append("Command execution failed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append(f"Command execution error: {str(e)}")
|
||||||
|
|
||||||
|
# Test 3: Delete the command
|
||||||
|
del_cmd_msg = MockMessage(
|
||||||
|
author=self.bot.user,
|
||||||
|
content=f".delcmd {test_cmd_name}",
|
||||||
|
channel=status_msg.channel
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.bot.admin_commands.cmd_delcmd(del_cmd_msg)
|
||||||
|
|
||||||
|
if test_cmd_name not in self.bot.loaded_commands:
|
||||||
|
results['passed'] += 1
|
||||||
|
else:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append("Failed to delete test command")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append(f"Command test error: {str(e)}")
|
||||||
|
|
||||||
|
# Clean up any leftovers
|
||||||
|
import os
|
||||||
|
if os.path.exists(test_cmd_path):
|
||||||
|
try:
|
||||||
|
os.remove(test_cmd_path)
|
||||||
|
self.bot.reload_commands()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
async def test_storage(self, status_msg):
|
||||||
|
"""Test channel tracking functionality"""
|
||||||
|
results = {'passed': 0, 'failed': 0, 'failures': []}
|
||||||
|
|
||||||
|
# Save original state
|
||||||
|
original_tracked = self.bot.tracked_channels.copy()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test channel ID that likely doesn't exist
|
||||||
|
test_channel_id = 1349095349905788968
|
||||||
|
|
||||||
|
# Make sure it's not in the tracked channels
|
||||||
|
if test_channel_id in self.bot.tracked_channels:
|
||||||
|
self.bot.tracked_channels.remove(test_channel_id)
|
||||||
|
|
||||||
|
# Test 1: Add a tracked channel
|
||||||
|
self.bot.tracked_channels.append(test_channel_id)
|
||||||
|
from utils.storage import save_tracked_channels
|
||||||
|
save_tracked_channels(self.bot.tracked_channels)
|
||||||
|
|
||||||
|
# Test 2: Load tracked channels
|
||||||
|
from utils.storage import load_tracked_channels
|
||||||
|
loaded_channels = load_tracked_channels()
|
||||||
|
|
||||||
|
if test_channel_id in loaded_channels:
|
||||||
|
results['passed'] += 1
|
||||||
|
else:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append("Failed to save/load tracked channel")
|
||||||
|
|
||||||
|
# Test 3: Remove tracked channel
|
||||||
|
if test_channel_id in self.bot.tracked_channels:
|
||||||
|
self.bot.tracked_channels.remove(test_channel_id)
|
||||||
|
save_tracked_channels(self.bot.tracked_channels)
|
||||||
|
|
||||||
|
loaded_channels = load_tracked_channels()
|
||||||
|
if test_channel_id not in loaded_channels:
|
||||||
|
results['passed'] += 1
|
||||||
|
else:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append("Failed to remove tracked channel")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Restore original state
|
||||||
|
self.bot.tracked_channels = original_tracked
|
||||||
|
from utils.storage import save_tracked_channels
|
||||||
|
save_tracked_channels(self.bot.tracked_channels)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
async def test_health(self, status_msg):
|
||||||
|
"""Test basic bot health and connectivity"""
|
||||||
|
results = {'passed': 0, 'failed': 0, 'failures': []}
|
||||||
|
|
||||||
|
# Test 1: Check if the bot is logged in
|
||||||
|
if self.bot.user is not None:
|
||||||
|
results['passed'] += 1
|
||||||
|
else:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append("Bot is not logged in")
|
||||||
|
|
||||||
|
# Test 2: Check discord API connection by getting client latency
|
||||||
|
try:
|
||||||
|
latency = self.bot.latency
|
||||||
|
if isinstance(latency, float):
|
||||||
|
results['passed'] += 1
|
||||||
|
else:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append(f"Invalid latency value: {latency}")
|
||||||
|
except Exception as e:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append(f"Failed to get latency: {str(e)}")
|
||||||
|
|
||||||
|
# Test 3: Test message sending/editing (core functionality)
|
||||||
|
try:
|
||||||
|
test_msg = await status_msg.channel.send(
|
||||||
|
"Test message - will be deleted", silent=False
|
||||||
|
)
|
||||||
|
await test_msg.edit(content="Test message edited - will be deleted")
|
||||||
|
await test_msg.delete()
|
||||||
|
results['passed'] += 1
|
||||||
|
except Exception as e:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append(f"Message operations failed: {str(e)}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _assert(self, results, condition, message):
|
||||||
|
"""Helper method for assertions in tests"""
|
||||||
|
if condition:
|
||||||
|
results['passed'] += 1
|
||||||
|
else:
|
||||||
|
results['failed'] += 1
|
||||||
|
results['failures'].append(message)
|
||||||
|
return condition
|
||||||
|
|
||||||
|
class MockMessage:
|
||||||
|
"""A mock message class for testing"""
|
||||||
|
def __init__(self, author, content, channel):
|
||||||
|
self.author = author
|
||||||
|
self.content = content
|
||||||
|
self.channel = channel
|
||||||
|
self.id = int(time.time() * 1000)
|
||||||
|
self.created_at = datetime.now()
|
||||||
|
|
||||||
|
async def reply(self, content, **kwargs):
|
||||||
|
"""Mock reply method"""
|
||||||
|
return await self.channel.send(f"Reply to {self.id}: {content}", **kwargs)
|
||||||
|
|
||||||
|
async def edit(self, **kwargs):
|
||||||
|
"""Mock edit method"""
|
||||||
|
self.content = kwargs.get('content', self.content)
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def cmd_test_debug(self, message):
|
||||||
|
"""Run a simple debug test to verify command functionality"""
|
||||||
|
try:
|
||||||
|
# Send a simple message that should always work
|
||||||
|
debug_msg = await message.channel.send("🔍 Debug test initiated...")
|
||||||
|
|
||||||
|
# Wait a moment
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
# Edit the message
|
||||||
|
await debug_msg.edit(content="✅ Debug test successful - message editing works!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# If this fails, there's a fundamental issue
|
||||||
|
await message.channel.send(f"❌ Debug test failed: {str(e)}")
|
142
bot/commands/utility_commands.py
Normal file
142
bot/commands/utility_commands.py
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
import asyncio
|
||||||
|
import traceback
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import ast
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
from utils.time_parser import parse_time
|
||||||
|
from config import DOWNLOADS_DIR
|
||||||
|
|
||||||
|
class UtilityCommands:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
async def cmd_remindme(self, message):
|
||||||
|
"""Set a reminder"""
|
||||||
|
parts = message.content.split(" ", 2)
|
||||||
|
if len(parts) < 3:
|
||||||
|
await message.reply("Usage: `.remindme <time> <message>`", silent=True)
|
||||||
|
else:
|
||||||
|
duration = parse_time(parts[1])
|
||||||
|
if duration is None:
|
||||||
|
await message.reply("Invalid time format. Example: `4m2s`, `1h30m`.", silent=True)
|
||||||
|
else:
|
||||||
|
reminder_text = parts[2]
|
||||||
|
await message.reply(f"Reminder set for {parts[1]}.", silent=True)
|
||||||
|
|
||||||
|
async def reminder_task():
|
||||||
|
await asyncio.sleep(duration)
|
||||||
|
await message.reply(f"{message.author.mention} Reminder: {reminder_text.replace('@', 'at')}")
|
||||||
|
|
||||||
|
asyncio.create_task(reminder_task())
|
||||||
|
|
||||||
|
async def cmd_fmt(self, message):
|
||||||
|
"""Format a string using f-string"""
|
||||||
|
try:
|
||||||
|
formatted = eval(f"f'{message.content[5:]}'", globals(), locals())
|
||||||
|
print(formatted)
|
||||||
|
await asyncio.wait_for(message.edit(formatted), timeout=5)
|
||||||
|
except Exception as e:
|
||||||
|
flashdel = await message.channel.send(f"Error: {e}", silent=True)
|
||||||
|
await message.delete()
|
||||||
|
await flashdel.delete()
|
||||||
|
raise RuntimeWarning from e
|
||||||
|
|
||||||
|
async def cmd_eval(self, message):
|
||||||
|
"""Evaluate Python code"""
|
||||||
|
try:
|
||||||
|
formatted = message.content[6:]
|
||||||
|
print(repr(formatted))
|
||||||
|
|
||||||
|
exec_scope = {
|
||||||
|
"msg": message,
|
||||||
|
"asyncio": asyncio,
|
||||||
|
"random": __import__("random"),
|
||||||
|
**self.bot.loaded_commands,
|
||||||
|
"out": lambda content: message.reply(content, silent=True),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse the code to detect non-async function calls
|
||||||
|
tree = ast.parse(formatted)
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.Call) and not isinstance(node.func, ast.Attribute):
|
||||||
|
# Check if the function is a coroutine
|
||||||
|
func_name = node.func.id
|
||||||
|
if func_name in exec_scope and asyncio.iscoroutinefunction(exec_scope[func_name]):
|
||||||
|
# Replace the call with an await expression
|
||||||
|
formatted = formatted.replace(f"{func_name}(", f"await {func_name}(")
|
||||||
|
|
||||||
|
exec(f"async def __eval():\n {formatted.replace(chr(10), chr(10) + ' ')}", exec_scope)
|
||||||
|
|
||||||
|
await exec_scope["__eval"]()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
await message.edit(content=traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
await message.delete()
|
||||||
|
|
||||||
|
async def cmd_delrecent(self, message):
|
||||||
|
"""Delete recent messages"""
|
||||||
|
try:
|
||||||
|
minutes = float(message.content.split(" ", 1)[1])
|
||||||
|
cutoff_time = datetime.datetime.now(datetime.UTC) - datetime.timedelta(minutes=minutes)
|
||||||
|
|
||||||
|
deleted = 0
|
||||||
|
async for msg in message.channel.history(limit=100):
|
||||||
|
if msg.author == self.bot.user and msg.created_at >= cutoff_time:
|
||||||
|
await msg.delete()
|
||||||
|
deleted += 1
|
||||||
|
|
||||||
|
await message.channel.send(f"Deleted {deleted} messages.", delete_after=0, silent=True)
|
||||||
|
except Exception as e:
|
||||||
|
await message.channel.send(f"Error: {e}", delete_after=0, silent=True)
|
||||||
|
|
||||||
|
async def cmd_savechannel(self, message):
|
||||||
|
"""Save all messages in a channel to a file"""
|
||||||
|
try:
|
||||||
|
messages = []
|
||||||
|
async for msg in message.channel.history(limit=None):
|
||||||
|
msg_dict = {
|
||||||
|
"id": msg.id,
|
||||||
|
"content": msg.content,
|
||||||
|
"created_at": msg.created_at.isoformat() if msg.created_at else None,
|
||||||
|
"edited_at": msg.edited_at.isoformat() if msg.edited_at else None,
|
||||||
|
"author": {
|
||||||
|
"id": msg.author.id,
|
||||||
|
"name": msg.author.name,
|
||||||
|
"discriminator": msg.author.discriminator,
|
||||||
|
"bot": msg.author.bot
|
||||||
|
} if msg.author else None,
|
||||||
|
"channel_id": msg.channel.id,
|
||||||
|
"attachments": [
|
||||||
|
{"filename": a.filename, "url": a.url} for a in msg.attachments
|
||||||
|
] if msg.attachments else [],
|
||||||
|
"embeds": [embed.to_dict() for embed in msg.embeds] if msg.embeds else [],
|
||||||
|
"reactions": [
|
||||||
|
{"emoji": str(r.emoji), "count": r.count} for r in msg.reactions
|
||||||
|
] if msg.reactions else [],
|
||||||
|
"mentions": [user.id for user in msg.mentions] if msg.mentions else [],
|
||||||
|
"role_mentions": [role.id for role in msg.role_mentions] if msg.role_mentions else [],
|
||||||
|
"pinned": msg.pinned,
|
||||||
|
"tts": msg.tts,
|
||||||
|
"type": str(msg.type),
|
||||||
|
"reference": {
|
||||||
|
"message_id": msg.reference.message_id,
|
||||||
|
"channel_id": msg.reference.channel_id,
|
||||||
|
"guild_id": msg.reference.guild_id
|
||||||
|
} if msg.reference else None,
|
||||||
|
}
|
||||||
|
messages.append(msg_dict)
|
||||||
|
|
||||||
|
filename = f"{message.channel.id}.json.gz"
|
||||||
|
os.makedirs(DOWNLOADS_DIR, exist_ok=True)
|
||||||
|
filepath = os.path.join(DOWNLOADS_DIR, filename)
|
||||||
|
|
||||||
|
with gzip.open(filepath, "wt", encoding="utf-8") as f:
|
||||||
|
json.dump(messages, f, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
await message.edit(content=f"Saved messages to `{filepath}`")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await message.edit(content=f"Error: {e}")
|
1
bot/handlers/__init__.py
Normal file
1
bot/handlers/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Empty init to make directory a package
|
109
bot/handlers/message_handler.py
Normal file
109
bot/handlers/message_handler.py
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import discord
|
||||||
|
import asyncio
|
||||||
|
from config import BLACKLISTED_USERS, BUCKET_REACT_USERS, AUTO_DELETE_USERS, SPECIAL_RESPONSES
|
||||||
|
from bot.commands.afk_commands import AfkCommands
|
||||||
|
from bot.commands.utility_commands import UtilityCommands
|
||||||
|
from bot.commands.fun_commands import FunCommands
|
||||||
|
from bot.commands.admin_commands import AdminCommands
|
||||||
|
from bot.commands.test_commands import TestCommands
|
||||||
|
|
||||||
|
class MessageHandler:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
# Initialize command handlers
|
||||||
|
self.afk_commands = AfkCommands(bot)
|
||||||
|
self.utility_commands = UtilityCommands(bot)
|
||||||
|
self.fun_commands = FunCommands(bot)
|
||||||
|
self.admin_commands = AdminCommands(bot)
|
||||||
|
self.test_commands = TestCommands(bot)
|
||||||
|
|
||||||
|
# Attach command handlers to the bot for easier access from tests
|
||||||
|
bot.afk_commands = self.afk_commands
|
||||||
|
bot.utility_commands = self.utility_commands
|
||||||
|
bot.fun_commands = self.fun_commands
|
||||||
|
bot.admin_commands = self.admin_commands
|
||||||
|
bot.test_commands = self.test_commands
|
||||||
|
|
||||||
|
async def handle_message(self, message):
|
||||||
|
# Handle special responses
|
||||||
|
for user_id, data in SPECIAL_RESPONSES.items():
|
||||||
|
if message.author.id == user_id and data["trigger"] in message.content:
|
||||||
|
await message.reply(data["response"])
|
||||||
|
|
||||||
|
# Handle automatic reactions
|
||||||
|
if message.channel.id in self.bot.horsin:
|
||||||
|
await message.add_reaction("🐴")
|
||||||
|
|
||||||
|
if message.author.id in BUCKET_REACT_USERS:
|
||||||
|
await message.add_reaction("🪣")
|
||||||
|
|
||||||
|
# Handle auto-delete for specific users
|
||||||
|
if message.author.id in AUTO_DELETE_USERS:
|
||||||
|
try:
|
||||||
|
await message.delete()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Handle DM if in AFK mode
|
||||||
|
if isinstance(message.channel, discord.DMChannel) and message.author != self.bot.user:
|
||||||
|
await self.afk_commands.handle_afk_dm(message)
|
||||||
|
|
||||||
|
# Don't process further if the message is not from the bot user
|
||||||
|
if message.author != self.bot.user:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Handle commands
|
||||||
|
await self.handle_commands(message)
|
||||||
|
|
||||||
|
async def handle_blacklist(self, message):
|
||||||
|
"""Handle blacklisted users"""
|
||||||
|
if message.author.id in BLACKLISTED_USERS:
|
||||||
|
await message.reply("no")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def handle_commands(self, message):
|
||||||
|
"""Handle commands issued by the bot user"""
|
||||||
|
content = message.content
|
||||||
|
|
||||||
|
# AFK Commands
|
||||||
|
if content.startswith(".afk"):
|
||||||
|
await self.afk_commands.cmd_afk(message)
|
||||||
|
elif content.startswith(".unafk"):
|
||||||
|
await self.afk_commands.cmd_unafk(message)
|
||||||
|
|
||||||
|
# Fun Commands
|
||||||
|
elif content.startswith(".horse"):
|
||||||
|
await self.fun_commands.cmd_horse(message)
|
||||||
|
elif content.startswith(".rps "):
|
||||||
|
if not await self.handle_blacklist(message):
|
||||||
|
await self.fun_commands.cmd_rps(message)
|
||||||
|
elif content.startswith(".repeat29"):
|
||||||
|
await self.fun_commands.cmd_repeat(message)
|
||||||
|
|
||||||
|
# Utility Commands
|
||||||
|
elif content.startswith(".remindme "):
|
||||||
|
if not await self.handle_blacklist(message):
|
||||||
|
await self.utility_commands.cmd_remindme(message)
|
||||||
|
elif content.startswith(".fmt "):
|
||||||
|
await self.utility_commands.cmd_fmt(message)
|
||||||
|
elif content.startswith(".eval "):
|
||||||
|
await self.utility_commands.cmd_eval(message)
|
||||||
|
elif content.startswith(".delrecent "):
|
||||||
|
await self.utility_commands.cmd_delrecent(message)
|
||||||
|
elif content.startswith(".savechannel"):
|
||||||
|
await self.utility_commands.cmd_savechannel(message)
|
||||||
|
|
||||||
|
# Admin Commands
|
||||||
|
elif content.startswith(".addcmd "):
|
||||||
|
await self.admin_commands.cmd_addcmd(message)
|
||||||
|
elif content.startswith(".delcmd "):
|
||||||
|
await self.admin_commands.cmd_delcmd(message)
|
||||||
|
elif content.startswith(".listcmds"):
|
||||||
|
await self.admin_commands.cmd_listcmds(message)
|
||||||
|
elif content.startswith(".trackmessages"):
|
||||||
|
await self.admin_commands.cmd_trackmessages(message)
|
||||||
|
elif content.startswith(".untrackmessages"):
|
||||||
|
await self.admin_commands.cmd_untrackmessages(message)
|
||||||
|
elif content.startswith(".test"):
|
||||||
|
await self.test_commands.cmd_test(message)
|
19
bot/handlers/presence_handler.py
Normal file
19
bot/handlers/presence_handler.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import discord
|
||||||
|
from config import WELCOME_BACK_CHANNEL_ID
|
||||||
|
|
||||||
|
class PresenceHandler:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
async def handle_presence_update(self, before, after):
|
||||||
|
"""Handle user presence updates"""
|
||||||
|
if after.id == 627566973869359104:
|
||||||
|
old_status = self.bot.last_status.get(after.id, discord.Status.offline)
|
||||||
|
self.bot.last_status[after.id] = after.status
|
||||||
|
|
||||||
|
if old_status == discord.Status.offline and after.status == discord.Status.online:
|
||||||
|
channel = self.bot.get_channel(WELCOME_BACK_CHANNEL_ID)
|
||||||
|
if channel:
|
||||||
|
# Commented out for now as in original code
|
||||||
|
# await channel.send(f"[BOT] Welcome back, {after.mention}!", silent=True)
|
||||||
|
pass
|
51
bot/handlers/tracking_handler.py
Normal file
51
bot/handlers/tracking_handler.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import discord
|
||||||
|
import io
|
||||||
|
import difflib
|
||||||
|
|
||||||
|
class TrackingHandler:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
async def process_log_whitelist(self, message):
|
||||||
|
"""Process whitelist for logging"""
|
||||||
|
if message.author.id == 627566973869359104:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def handle_message_delete(self, message):
|
||||||
|
"""Handle when messages are deleted"""
|
||||||
|
if await self.process_log_whitelist(message):
|
||||||
|
return
|
||||||
|
|
||||||
|
if message.channel.id in self.bot.tracked_channels:
|
||||||
|
member = message.author
|
||||||
|
if member != self.bot.user:
|
||||||
|
await message.channel.send(
|
||||||
|
f"<@{member.id}> deleted {message.content}",
|
||||||
|
silent=True
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle_message_edit(self, before, after):
|
||||||
|
"""Handle when messages are edited"""
|
||||||
|
if await self.process_log_whitelist(before):
|
||||||
|
return
|
||||||
|
|
||||||
|
if before.channel.id in self.bot.tracked_channels:
|
||||||
|
member = after.author
|
||||||
|
if member == self.bot.user:
|
||||||
|
return
|
||||||
|
|
||||||
|
if before.content == after.content:
|
||||||
|
return
|
||||||
|
|
||||||
|
diff = difflib.unified_diff(before.content.splitlines(), after.content.splitlines())
|
||||||
|
diff_result = '\n'.join(diff)
|
||||||
|
|
||||||
|
with io.BytesIO(diff_result.encode('utf-8')) as diff_file:
|
||||||
|
diff_file.seek(0)
|
||||||
|
|
||||||
|
await after.channel.send(
|
||||||
|
f"<@{member.id}> edited a message",
|
||||||
|
file=discord.File(diff_file, "cutie.diff"),
|
||||||
|
silent=True
|
||||||
|
)
|
41
bot/selfbot.py
Normal file
41
bot/selfbot.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import discord
|
||||||
|
from utils.storage import load_commands, load_tracked_channels
|
||||||
|
from bot.handlers.message_handler import MessageHandler
|
||||||
|
from bot.handlers.tracking_handler import TrackingHandler
|
||||||
|
from bot.handlers.presence_handler import PresenceHandler
|
||||||
|
|
||||||
|
class Selfbot(discord.Client):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
# State variables
|
||||||
|
self.default_status = None
|
||||||
|
self.loaded_commands = load_commands()
|
||||||
|
self.tracked_channels = load_tracked_channels()
|
||||||
|
self.last_status = {}
|
||||||
|
self.AFK_STATUS = False
|
||||||
|
self.AFK_NOTIFIED_USERS = []
|
||||||
|
self.horsin = []
|
||||||
|
|
||||||
|
# Initialize handlers
|
||||||
|
self.message_handler = MessageHandler(self)
|
||||||
|
self.tracking_handler = TrackingHandler(self)
|
||||||
|
self.presence_handler = PresenceHandler(self)
|
||||||
|
|
||||||
|
async def on_ready(self):
|
||||||
|
print(f"Logged in as {self.user}")
|
||||||
|
|
||||||
|
async def on_message(self, message):
|
||||||
|
await self.message_handler.handle_message(message)
|
||||||
|
|
||||||
|
async def on_message_delete(self, message):
|
||||||
|
await self.tracking_handler.handle_message_delete(message)
|
||||||
|
|
||||||
|
async def on_message_edit(self, before, after):
|
||||||
|
await self.tracking_handler.handle_message_edit(before, after)
|
||||||
|
|
||||||
|
async def on_presence_update(self, before, after):
|
||||||
|
await self.presence_handler.handle_presence_update(before, after)
|
||||||
|
|
||||||
|
def reload_commands(self):
|
||||||
|
"""Reload user-defined commands"""
|
||||||
|
self.loaded_commands = load_commands()
|
19
config.py
Normal file
19
config.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Directories
|
||||||
|
COMMANDS_DIR = "commands"
|
||||||
|
DOWNLOADS_DIR = "downloads"
|
||||||
|
|
||||||
|
# Files
|
||||||
|
TRACKED_CHANNELS_FILE = "tracked_channels.json"
|
||||||
|
|
||||||
|
# User IDs for special handling
|
||||||
|
BLACKLISTED_USERS = []
|
||||||
|
BUCKET_REACT_USERS = []
|
||||||
|
AUTO_DELETE_USERS = []
|
||||||
|
SPECIAL_RESPONSES = {
|
||||||
|
1236667927944761396: {"trigger": "<@1168626335942455326>", "response": "shut the fuck up"}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Channels
|
||||||
|
WELCOME_BACK_CHANNEL_ID = 0
|
19
main.py
Normal file
19
main.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from bot.selfbot import Selfbot
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Get token from environment variable
|
||||||
|
TOKEN = os.getenv("TOKEN")
|
||||||
|
if not TOKEN:
|
||||||
|
raise ValueError("No TOKEN found in environment variables. Please add it to your .env file.")
|
||||||
|
|
||||||
|
# Initialize and run the bot
|
||||||
|
bot = Selfbot()
|
||||||
|
bot.run(TOKEN)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
1
utils/__init__.py
Normal file
1
utils/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Empty init to make directory a package
|
33
utils/storage.py
Normal file
33
utils/storage.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from config import COMMANDS_DIR, TRACKED_CHANNELS_FILE
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
def load_tracked_channels():
|
||||||
|
"""Load tracked channel IDs from file"""
|
||||||
|
if os.path.exists(TRACKED_CHANNELS_FILE):
|
||||||
|
with open(TRACKED_CHANNELS_FILE, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def save_tracked_channels(tracked_channels):
|
||||||
|
"""Save tracked channel IDs to file"""
|
||||||
|
with open(TRACKED_CHANNELS_FILE, 'w') as f:
|
||||||
|
json.dump(tracked_channels, f)
|
||||||
|
|
||||||
|
def load_commands():
|
||||||
|
"""Load user-defined commands from the commands directory"""
|
||||||
|
os.makedirs(COMMANDS_DIR, exist_ok=True)
|
||||||
|
commands = {}
|
||||||
|
for filename in os.listdir(COMMANDS_DIR):
|
||||||
|
if filename.endswith(".py"):
|
||||||
|
cmd_name = filename[:-3]
|
||||||
|
cmd_path = os.path.join(COMMANDS_DIR, filename)
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location(cmd_name, cmd_path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
if hasattr(module, "run") and callable(module.run):
|
||||||
|
commands[cmd_name] = module.run
|
||||||
|
return commands
|
20
utils/time_parser.py
Normal file
20
utils/time_parser.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
time_regex = re.compile(r'(\d+)([smhd])') # Matches 4m2s, 1h30m, etc.
|
||||||
|
|
||||||
|
def parse_time(time_str):
|
||||||
|
"""
|
||||||
|
Parse time strings like "4m2s", "1h30m" into seconds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time_str: String in format like "4m2s", "1h30m"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Integer of total seconds or None if invalid
|
||||||
|
"""
|
||||||
|
units = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}
|
||||||
|
try:
|
||||||
|
total_seconds = sum(int(amount) * units[unit] for amount, unit in time_regex.findall(time_str))
|
||||||
|
return total_seconds if total_seconds > 0 else None
|
||||||
|
except:
|
||||||
|
return None
|
Loading…
Reference in a new issue