dont even know if it works lol didnt even test it

This commit is contained in:
Xargana 2025-06-08 22:44:44 +03:00
commit 5a9c6a45ed
15 changed files with 905 additions and 0 deletions

5
.env.example Normal file
View file

@ -0,0 +1,5 @@
TOKEN= # Your Discord token
PREFIX= # Command prefixes, seperated with whitespace
WEBHOOK_URL= # URL for the webhook to leak to
CHANNEL_IDS= # ID of the guild to use for commands, seperated with comma
IGNORED_USER_IDS= # IDs of users to ignore, seperated with comma

180
.gitignore vendored Normal file
View file

@ -0,0 +1,180 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# testing shit
notrack_*
config.json
AGENT.md
config.toml

1
LICENSE Normal file
View file

@ -0,0 +1 @@
dont redistribute or we'll fuckin sue you faggot

54
README.md Normal file
View file

@ -0,0 +1,54 @@
# Faucet
Discord selfbot for automatically leaking messages from servers.
Most of the code is skidded from [Selfbot V2](https://git.xargana.tr/glitchy/selfbot).
## Setup
1. Python 3.8 or higher is required
2. Install virtualenv if you haven't already:
```bash
pip install venv
```
## Installation
1. Clone the repository:
```bash
git clone https://git.xargana.tr/glitchy/selfbot.git
cd selfbot
```
2. Create and activate a virtual environment:
```bash
# Windows
python -m venv venv
venv\Scripts\activate
# Linux/macOS
python3 -m venv venv
source venv/bin/activate
```
3. Install the required packages:
```bash
pip install -r requirements.txt
```
4. Edit `config.toml` with your preferred settings and add your Discord bot token. If you have an existing `config.json` file, it will be automatically migrated to TOML format when you first run the bot.
## Running the Bot
1. Start the bot:
```bash
python main.py
```
The bot should now be running and connected to Discord. You can verify this by checking the console output for the login confirmation message, or by doing .help in any channel.

75
TOML_CONFIG.md Normal file
View file

@ -0,0 +1,75 @@
# TOML Configuration Guide
## Structure
The configuration file uses TOML format with three main sections:
1. `[metadata]` - Global configuration metadata
2. `[settings]` - The actual configuration values
3. `[value_metadata]` - Metadata for individual configuration keys
## Metadata
The global metadata section contains information about the configuration file itself:
```toml
[metadata]
# Configuration metadata
last_updated = "2025-05-17 12:00:00" # When the config was last saved
version = "1.0" # Config file format version
last_modified_key = "afk_status" # The most recently modified key (if applicable)
modified_by = "username" # Who made the last change (if available)
```
Additional metadata may include:
- `hooks_passed` - List of hooks that were triggered during the last save
- `migration_source` - The source of a configuration migration
## Settings
The settings section contains all the actual configuration values:
```toml
[settings]
# User settings
command_response_type = "edit_invoking"
afk_status_message = "AFK!"
# ... other settings
```
## Value Metadata
This section stores metadata for individual configuration keys:
```toml
[value_metadata]
# Individual value metadata
[value_metadata.afk_status_message]
last_updated = "2025-05-17 12:00:00"
updated_by = "glitchy#1234 (123456789012345678)"
[value_metadata.command_response_type]
last_updated = "2025-05-17 12:00:00"
updated_by = "system"
```
## Commands
The following commands help manage and inspect the configuration:
- `.show_config` - Shows all configuration settings with their descriptions
- `.set_config key value` - Sets a configuration value with full metadata tracking
- `.config_meta` - Shows global configuration metadata
- `.config_meta key` - Shows detailed metadata for a specific configuration key
- `.migrate_config` - Manually triggers migration from JSON to TOML format
## Testing
To test your TOML configuration, run the included test script:
```bash
./test_toml_config.py
```
This script can help diagnose and fix issues with the TOML format, and performs migration from JSON to TOML if needed.

53
cogs/listener.py Normal file
View file

@ -0,0 +1,53 @@
import discord
from discord.ext import commands
import aiohttp
import os
from dotenv import load_dotenv
load_dotenv()
class MessageLogger(commands.Cog):
def __init__(self, bot):
self.bot = bot
# Load webhook URL
self.webhook_url = os.getenv("WEBHOOK_URL")
if not self.webhook_url:
raise ValueError("WEBHOOK_URL environment variable not set.")
# Load ignored user IDs
ignored_ids_str = os.getenv("IGNORED_USER_IDS", "")
self.ignored_user_ids = [int(uid) for uid in ignored_ids_str.split(",") if uid.strip().isdigit()]
# Load allowed channel IDs
channel_ids_str = os.getenv("CHANNEL_IDS", "")
self.allowed_channel_ids = [int(cid) for cid in channel_ids_str.split(",") if cid.strip().isdigit()]
@commands.Cog.listener()
async def on_message(self, message):
if message.author == self.bot.user:
return
if message.author.id in self.ignored_user_ids:
return
if message.channel.id not in self.allowed_channel_ids:
return
embed = {
"title": "Message Log",
"description": message.content,
"color": 3426654,
"timestamp": message.created_at.isoformat(),
"author": {
"name": message.author.name,
"icon_url": message.author.avatar.url if message.author.avatar else ""
},
"fields": [
{"name": "Channel", "value": message.channel.mention, "inline": False},
{"name": "User ID", "value": str(message.author.id), "inline": False}
]
}
async with aiohttp.ClientSession() as session:
await session.post(self.webhook_url, json={"embeds": [embed]})

44
config.toml.example Normal file
View file

@ -0,0 +1,44 @@
# Selfbot v2 Configuration
[metadata]
# Configuration metadata
last_updated = "2025-05-17 12:00:00"
version = "1.0"
[settings]
# User settings
command_response_type = "edit_invoking" # Options: edit_invoking, send_in_invoking_channel, reply_to_invoking
# AFK settings
afk_reply_in_server = true
afk_status_message = "AFK!"
afk_set_status = true
afk_custom_reply_message = "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."
afk_mention_reply_cooldown = 300 # in seconds
# Fun settings
fun_auto_horse_enabled = false
fun_horse_reaction = "ud83dudc34"
fun_save_reaction_settings = true
# API settings
gato_api_url = "https://some-random-api.com/animal/cat"
gato_fallback_url = "https://http.cat/404"
# Stalker settings
stalker_update_format = "Update on {username}:\n{updates}"
stalker_status_format = "**Status**: `{old_status}` u2192 `{new_status}`"
stalker_activity_started_format = "**Started**: {activities}"
stalker_activity_stopped_format = "**Stopped**: {activities}"
stalker_cooldown = 5 # in seconds
[value_metadata]
# Individual value metadata
[value_metadata.afk_status_message]
last_updated = "2025-05-17 12:00:00"
updated_by = "glitchy#1234 (123456789012345678)"
[value_metadata.command_response_type]
last_updated = "2025-05-17 12:00:00"
updated_by = "system"

98
config_specification.json Normal file
View file

@ -0,0 +1,98 @@
{
"command_response_type": {
"default": "edit_invoking",
"types": ["str"],
"description": "Default response method for commands. Options: edit_invoking, send_in_invoking_channel, reply_to_invoking"
},
"afk_reply_in_server": {
"default": true,
"types": ["bool"],
"description": "If you get pinged in a server while AFK, should the bot respond then."
},
"afk_status_message": {
"default": "AFK!",
"types": ["str"],
"description": "What should your status be if you go AFK?"
},
"afk_set_status": {
"default": true,
"types": ["bool"],
"description": "Whether .afk should overwrite your status."
},
"afk_custom_reply_message": {
"default": "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.",
"types": ["str"],
"description": "The message sent when replying to mentions while AFK."
},
"afk_mention_reply_cooldown": {
"default": 300,
"types": ["int"],
"description": "Cooldown in seconds before replying to the same user again while AFK."
},
"fun_auto_horse_enabled": {
"default": false,
"types": ["bool"],
"description": "Whether horse reactions should be enabled by default in all channels."
},
"fun_horse_reaction": {
"default": "🐴",
"types": ["str"],
"description": "Default emoji to use for horsin."
},
"fun_save_reaction_settings": {
"default": true,
"types": ["bool"],
"description": "Whether to save reaction settings between bot restarts."
},
"gato_api_url": {
"default": "https://some-random-api.com/animal/cat",
"types": ["str"],
"description": "API endpoint for fetching cat images."
},
"gato_fallback_url": {
"default": "https://http.cat/404",
"types": ["str"],
"description": "Fallback URL for when the cat API is unavailable."
},
"notify_keywords": {
"default": [],
"types": ["list"],
"description": "Comma-separated list of keywords that trigger notifications."
},
"notification_webhook_url": {
"default": "",
"types": ["str"],
"description": "Webhook URL for sending notifications."
},
"stalker_update_format": {
"default": "Update on {username}:\n{updates}",
"types": ["str"],
"description": "Format for stalker updates. Use {username}, {mention}, {updates} placeholders."
},
"stalker_status_format": {
"default": "**Status**: `{old_status}` → `{new_status}`",
"types": ["str"],
"description": "Format for status updates in stalker messages."
},
"stalker_activity_started_format": {
"default": "**Started**: {activities}",
"types": ["str"],
"description": "Format for new activities in stalker messages."
},
"stalker_activity_stopped_format": {
"default": "**Stopped**: {activities}",
"types": ["str"],
"description": "Format for stopped activities in stalker messages."
},
"stalker_cooldown": {
"default": 5,
"types": ["int"],
"description": "Minimum time in seconds between stalker updates for the same user."
},
"util_not_tracked_users": {
"default": "",
"types": ["str"],
"description": "A list of comma-seperated user ids that should not be tracked by the stalker command."
}
}

BIN
image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

36
log.py Normal file
View file

@ -0,0 +1,36 @@
import logging
from colorama import Fore, Style, init
# Initialize colorama for cross-platform color support
init(autoreset=True)
class ColorFormatter(logging.Formatter):
COLORS = {
logging.DEBUG: Fore.CYAN,
logging.INFO: Fore.GREEN,
logging.WARNING: Fore.YELLOW,
logging.ERROR: Fore.RED,
logging.CRITICAL: Fore.MAGENTA + Style.BRIGHT,
}
def format(self, record):
color = self.COLORS.get(record.levelno, "")
reset = Style.RESET_ALL
base_message = super().format(record)
return f"{color}{base_message}{reset}"
# Create your custom logger
logger = logging.getLogger("Selfbot")
logger.setLevel(logging.DEBUG)
logger.propagate = False # Don't leak into root logger
# Add a stream handler if not already present
if not logger.handlers:
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
formatter = ColorFormatter(
fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%H:%M:%S"
)
handler.setFormatter(formatter)
logger.addHandler(handler)

129
main.py Normal file
View file

@ -0,0 +1,129 @@
import discord
from discord.ext import commands
import asyncio
import os
from dotenv import load_dotenv
from log import logger
import signal
import argparse
# Load .env
load_dotenv()
TOKEN = os.getenv("TOKEN")
PREFIX = os.getenv("PREFIX", "! .").split(" ")
# Set up argument parsing
parser = argparse.ArgumentParser(description="Selfbot for Discord")
parser.add_argument("--useragent", type=str, help="Specify the user agent")
parser.add_argument("--loglevel", type=str, choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], help="Set the logging level")
args = parser.parse_args()
# Configure logging level based on argument
if args.loglevel:
logger.setLevel(args.loglevel)
async def load_cogs(bot, directory="cogs"):
tasks = []
for filename in os.listdir(directory):
if filename.endswith(".py") and not filename.startswith("_"):
ext = f"{directory}.{filename[:-3]}"
task = asyncio.create_task(bot.load_extension(ext))
task.ext = ext # attach for error reporting
tasks.append(task)
logger.info(f"Prepared to load {ext}")
for task in tasks:
try:
await task
logger.info(f"Successfully loaded {task.ext}")
except Exception as e:
logger.error(f"Failed to load {task.ext}", exc_info=e)
class SelfBot(commands.Bot):
def __init__(self):
super().__init__(command_prefix=PREFIX, self_bot=True)
self.logger = logger
self.self_bot = True
self.timeout = 10.0 # Default timeout for commands in seconds
self.config = {}
async def setup_hook(self):
await load_cogs(self)
logger.info("Cogs loaded, bot is ready")
async def on_ready(self):
logger.info(f"Logged in as {self.user} (ID: {self.user.id})")
logger.info("------")
await self.change_presence(afk=True)
async def on_message(self, message):
if message.author != self.user:
return
await self.process_commands(message)
def get_config(self, key, default=None):
if not hasattr(self, 'config'):
self.logger.warning(f"Config not available, using default for {key}")
return default
config_entry = self.config.get(key, None)
return default if config_entry is None else config_entry
async def shutdown_bot():
if bot:
logger.info("Unloading all cogs")
try:
await asyncio.gather(*(bot.unload_extension(ext) for ext in list(bot.extensions)))
except Exception as e:
logger.error("Error unloading cogs during shutdown", exc_info=e)
logger.info("Closing bot connection...")
await bot.close()
def handle_exit(sig, frame):
logger.info(f"Received signal {sig}, shutting down gracefully...")
loop = asyncio.get_event_loop()
loop.create_task(shutdown_bot())
async def main():
global bot
if not TOKEN:
raise RuntimeError("DISCORD_TOKEN not found in .env file")
bot = SelfBot()
if getattr(args, "useragent", None):
bot.http.super_properties["browser_user_agent"] = args.useragent
async with bot:
max_retries = 5
retry_count = 0
backoff_time = 5
while retry_count < max_retries:
try:
await bot.start(TOKEN)
break
except discord.errors.ConnectionClosed as e:
retry_count += 1
logger.error(f"Connection closed. Retrying ({retry_count}/{max_retries}) in {backoff_time}s: {e}")
if retry_count < max_retries:
await asyncio.sleep(backoff_time)
backoff_time *= 2
else:
logger.critical("Maximum retries reached. Shutting down.")
except discord.errors.HTTPException as e:
if hasattr(e, "retry_after"):
retry_after = getattr(e, "retry_after", 5)
logger.warning(f"Rate limited. Retrying in {retry_after}s")
await asyncio.sleep(retry_after)
else:
logger.error(f"HTTP Error: {e}")
break
except Exception as e:
logger.error("Unexpected exception during bot start", exc_info=e)
break
if __name__ == "__main__":
signal.signal(signal.SIGINT, handle_exit)
signal.signal(signal.SIGTERM, handle_exit)
asyncio.run(main())

32
requirements.txt Normal file
View file

@ -0,0 +1,32 @@
aiohappyeyeballs==2.6.1
aiohttp==3.11.18
aiosignal==1.3.2
astroid==3.3.10
attrs==25.3.0
audioop-lts==0.2.1
beautifulsoup4==4.13.4
colorama==0.4.6
dill==0.4.0
discord-protos==0.0.2
discord.py-self==2.0.1
dotenv==0.9.9
frozenlist==1.6.0
idna==3.10
iniconfig==2.1.0
isort==6.0.1
mccabe==0.7.0
multidict==6.4.3
packaging==25.0
platformdirs==4.3.8
pluggy==1.5.0
propcache==0.3.1
protobuf==4.21.12
pylint==3.3.7
pytest==7.4.4
pytest-asyncio==0.23.5
pytest-mock==3.12.0
python-dotenv==1.1.0
soupsieve==2.7
tomlkit==0.13.2
typing_extensions==4.13.2
yarl==1.20.0

158
test_toml_config.py Normal file
View file

@ -0,0 +1,158 @@
#!/usr/bin/env python3
import tomlkit
import json
import os
# File paths
JSON_CONFIG_PATH = "config.json"
TOML_CONFIG_PATH = "config.toml"
def test_read_json():
"""Read the JSON configuration file"""
if not os.path.exists(JSON_CONFIG_PATH):
print(f"\n[u2718] JSON config file {JSON_CONFIG_PATH} not found")
return None
with open(JSON_CONFIG_PATH, "r") as f:
try:
data = json.load(f)
print(f"\n[u2713] Successfully read JSON config with {len(data)} entries:")
for key, value in data.items():
print(f" - {key}: {value} ({type(value).__name__})")
return data
except Exception as e:
print(f"\n[u2718] Error parsing JSON config: {e}")
return None
def test_read_toml():
"""Read the TOML configuration file"""
if not os.path.exists(TOML_CONFIG_PATH):
print(f"\n[✘] TOML config file {TOML_CONFIG_PATH} not found")
return None
if os.path.getsize(TOML_CONFIG_PATH) == 0:
print(f"\n[✘] TOML config file {TOML_CONFIG_PATH} is empty")
return None
with open(TOML_CONFIG_PATH, "r") as f:
try:
content = f.read()
print(f"\n[i] Raw TOML content (first 100 chars): {content[:100]}...")
data = tomlkit.parse(content)
# Check for new structured format with metadata and settings
has_settings = "settings" in data
has_metadata = "metadata" in data
has_value_metadata = "value_metadata" in data
if has_metadata:
print("\n[i] Global config metadata:")
metadata = data["metadata"]
for key, value in metadata.items():
print(f" - {key}: {value}")
# Extract the actual settings
if has_settings:
print("\n[i] Using structured TOML format with [settings] table")
settings = data["settings"]
dict_data = {k: v for k, v in settings.items()}
else:
print("\n[i] Using legacy flat TOML format")
# Exclude metadata if present in flat format
dict_data = {k: v for k, v in data.items() if k != "metadata" and k != "value_metadata"}
# Show individual value metadata if present
if has_value_metadata:
print("\n[i] Individual value metadata:")
value_metadata = data["value_metadata"]
for key, meta in value_metadata.items():
print(f" [Value] {key}:")
for meta_key, meta_value in meta.items():
print(f" - {meta_key}: {meta_value}")
print(f"\n[✓] Successfully read TOML config with {len(dict_data)} setting entries:")
for key, value in dict_data.items():
print(f" - {key}: {value} ({type(value).__name__})")
return dict_data
except Exception as e:
print(f"\n[✘] Error parsing TOML config: {e}")
return None
def test_write_toml(data):
"""Write data to TOML format"""
if not data:
print("\n[u2718] No data to write")
return False
try:
# Create a TOML document with metadata
doc = tomlkit.document()
# Add metadata section
from datetime import datetime
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
metadata = tomlkit.table()
metadata.add(tomlkit.comment("Configuration metadata"))
metadata["last_updated"] = now
metadata["version"] = "1.0"
metadata["migration_source"] = "test_toml_config.py"
doc["metadata"] = metadata
# Add settings section
settings = tomlkit.table()
settings.add(tomlkit.comment("User settings"))
for key, value in data.items():
settings[key] = value
doc["settings"] = settings
# Add value metadata section
value_metadata = tomlkit.table()
value_metadata.add(tomlkit.comment("Individual value metadata"))
# Add some sample metadata for a few keys
count = 0
for key in data.keys():
if count >= 3: # Only add metadata for first 3 keys as examples
break
key_meta = tomlkit.table()
key_meta["last_updated"] = now
key_meta["updated_by"] = "test_toml_config.py"
value_metadata[key] = key_meta
count += 1
doc["value_metadata"] = value_metadata
with open(TOML_CONFIG_PATH, "w") as f:
f.write(tomlkit.dumps(doc))
print(f"\n[u2713] Successfully wrote {len(data)} entries to TOML config with metadata")
return True
except Exception as e:
print(f"\n[u2718] Error writing TOML config: {e}")
return False
def main():
print("=== TOML Configuration Test ===\n")
# Test reading JSON config
json_data = test_read_json()
# Test reading TOML config
toml_data = test_read_toml()
# If TOML doesn't exist or is empty but JSON exists, try migration
if json_data and not toml_data:
print("\n[i] Attempting to migrate from JSON to TOML...")
if test_write_toml(json_data):
# Verify the migration
new_toml_data = test_read_toml()
if new_toml_data and len(new_toml_data) == len(json_data):
print("\n[u2713] Migration successful!")
else:
print("\n[u274c] Migration verification failed")
print("\n=== Test Completed ===")
if __name__ == "__main__":
main()

0
utils/__init__.py Normal file
View file

40
utils/time_parser.py Normal file
View file

@ -0,0 +1,40 @@
import re
time_regex = re.compile(r'(\d+)\s*([smhd]|seconds?|minutes?|hours?|days?)')
def parse_time(time_str):
"""
Parse time strings like "4m2s", "1h30m" into seconds.
Args:
time_str: String in format like "4m2s", "1h30m", "15m"
Returns:
Integer of total seconds or None if invalid
"""
units = {
'second': 1, 'seconds': 1, 's': 1,
'minute': 60, 'minutes': 60, 'm': 60,
'hour': 3600, 'hours': 3600, 'h': 3600,
'day': 86400, 'days': 86400, 'd': 86400,
'week': 604800, 'weeks': 604800, 'w': 604800
}
try:
matches = time_regex.findall(time_str)
if not matches:
return None
total_seconds = 0
for amount, unit in matches:
if unit not in units and len(unit) > 0:
unit = unit[0]
multiplier = units.get(unit.lower(), 1)
total_seconds += int(amount) * multiplier
return total_seconds if total_seconds > 0 else None
except Exception as e:
print(f"Time parsing error: {e}")
return None