@@ -8,6 +8,7 @@ from dotenv import load_dotenv
import pytz
import pytz
import random
import random
import qbittorrentapi
import qbittorrentapi
from proxmoxer import ProxmoxAPI
# =====================
# =====================
# ENV + VALIDATION
# ENV + VALIDATION
@@ -40,6 +41,8 @@ JELLYSEERR_API_KEY = os.getenv("JELLYSEERR_API_KEY", "")
ENABLE_JFA = os . getenv ( " ENABLE_JFA " , " False " ) . lower ( ) == " true "
ENABLE_JFA = os . getenv ( " ENABLE_JFA " , " False " ) . lower ( ) == " true "
JFA_URL = os . getenv ( " JFA_URL " )
JFA_URL = os . getenv ( " JFA_URL " )
JFA_USERNAME = os . getenv ( " JFA_USERNAME " )
JFA_PASSWORD = os . getenv ( " JFA_PASSWORD " )
JFA_API_KEY = os . getenv ( " JFA_API_KEY " )
JFA_API_KEY = os . getenv ( " JFA_API_KEY " )
ENABLE_QBITTORRENT = os . getenv ( " ENABLE_QBITTORRENT " , " False " ) . lower ( ) == " true "
ENABLE_QBITTORRENT = os . getenv ( " ENABLE_QBITTORRENT " , " False " ) . lower ( ) == " true "
@@ -47,6 +50,12 @@ QBIT_HOST = os.getenv("QBIT_HOST")
QBIT_USERNAME = os . getenv ( " QBIT_USERNAME " )
QBIT_USERNAME = os . getenv ( " QBIT_USERNAME " )
QBIT_PASSWORD = os . getenv ( " QBIT_PASSWORD " )
QBIT_PASSWORD = os . getenv ( " QBIT_PASSWORD " )
ENABLE_PROXMOX = os . getenv ( " ENABLE_PROXMOX " , " False " ) . lower ( ) == " true "
PROXMOX_HOST = os . getenv ( " PROXMOX_HOST " )
PROXMOX_TOKEN_NAME = os . getenv ( " PROXMOX_TOKEN_NAME " )
PROXMOX_TOKEN_VALUE = os . getenv ( " PROXMOX_TOKEN_VALUE " )
PROXMOX_VERIFY_SSL = os . getenv ( " PROXMOX_VERIFY_SSL " , " False " ) . lower ( ) == " true "
DB_HOST = get_env_var ( " DB_HOST " )
DB_HOST = get_env_var ( " DB_HOST " )
DB_USER = get_env_var ( " DB_USER " )
DB_USER = get_env_var ( " DB_USER " )
DB_PASSWORD = get_env_var ( " DB_PASSWORD " )
DB_PASSWORD = get_env_var ( " DB_PASSWORD " )
@@ -54,7 +63,7 @@ DB_NAME = get_env_var("DB_NAME")
LOCAL_TZ = pytz . timezone ( get_env_var ( " LOCAL_TZ " , str , required = False ) or " America/Chicago " )
LOCAL_TZ = pytz . timezone ( get_env_var ( " LOCAL_TZ " , str , required = False ) or " America/Chicago " )
BOT_VERSION = " 1.0.6 "
BOT_VERSION = " 1.0.7 "
VERSION_URL = " https://raw.githubusercontent.com/PenguCCN/Jellycord/main/version.txt "
VERSION_URL = " https://raw.githubusercontent.com/PenguCCN/Jellycord/main/version.txt "
RELEASES_URL = " https://github.com/PenguCCN/Jellycord/releases "
RELEASES_URL = " https://github.com/PenguCCN/Jellycord/releases "
@@ -81,15 +90,23 @@ bot = commands.Bot(command_prefix=PREFIX, intents=intents, help_command=None)
# QBITTORRENT SETUP
# QBITTORRENT SETUP
# =====================
# =====================
qb = qbittorrentapi . Client (
if ENABLE_QBITTORRENT :
host = QBIT_HOST ,
username = QBIT_USERNAME ,
qb = qbittorrentapi . Client (
password = QBIT_PASSWORD
host = QBIT_HOST ,
)
username = QBIT_USERNAME ,
try :
password = QBIT_PASSWORD
qb . auth_log_in ( )
)
except qbittorrentapi . LoginFailed :
print ( " Failed to log in to qBittorrent API " )
try :
qb . auth_log_in ( )
print ( " ✅ Logged in to qBittorrent " )
except qbittorrentapi . LoginFailed :
print ( " ❌ Failed to log in to qBittorrent API " )
qb = None
else :
qb = None # qBittorrent disabled
# =====================
# =====================
# DATABASE SETUP
# DATABASE SETUP
@@ -282,6 +299,32 @@ def reset_jellyfin_password(username: str, new_password: str) -> bool:
response = requests . post ( f " { JELLYFIN_URL } /Users/ { user_id } /Password " , headers = headers , json = data )
response = requests . post ( f " { JELLYFIN_URL } /Users/ { user_id } /Password " , headers = headers , json = data )
return response . status_code in ( 200 , 204 )
return response . status_code in ( 200 , 204 )
def create_trial_jellyfin_user ( username , password ) :
payload = {
" Name " : username ,
" Password " : password ,
" Policy " : {
" EnableDownloads " : False ,
" EnableSyncTranscoding " : False ,
" EnableRemoteControlOfOtherUsers " : False ,
" EnableLiveTvAccess " : False ,
" IsAdministrator " : False ,
" IsHidden " : False ,
" IsDisabled " : False
}
}
headers = {
" X-Emby-Token " : JELLYFIN_API_KEY ,
" Content-Type " : " application/json "
}
response = requests . post ( f " { JELLYFIN_URL } /Users/New " , json = payload , headers = headers )
if response . status_code == 200 :
return response . json ( ) . get ( " Id " )
else :
print ( f " [Jellyfin] Trial user creation failed. Status: { response . status_code } , Response: { response . text } " )
return None
# =====================
# =====================
# JELLYSEERR HELPERS
# JELLYSEERR HELPERS
# =====================
# =====================
@@ -348,6 +391,109 @@ def progress_bar(progress: float, length: int = 20) -> str:
bar = ' █ ' * filled_length + ' ░ ' * ( length - filled_length )
bar = ' █ ' * filled_length + ' ░ ' * ( length - filled_length )
return f " [ { bar } ] { progress * 100 : .2f } % "
return f " [ { bar } ] { progress * 100 : .2f } % "
# =====================
# PROXMOX HELPERS
# =====================
def get_proxmox_client ( ) :
""" Create and return a Proxmox client using API token auth. """
if not ( PROXMOX_HOST and PROXMOX_TOKEN_NAME and PROXMOX_TOKEN_VALUE ) :
raise ValueError ( " Proxmox API credentials are not fully configured in .env " )
# Parse host + port safely
base_host = PROXMOX_HOST . replace ( " https:// " , " " ) . replace ( " http:// " , " " )
if " : " in base_host :
host , port = base_host . split ( " : " )
else :
host , port = base_host , 8006 # default Proxmox port
# Split token into user + token name
try :
user , token_name = PROXMOX_TOKEN_NAME . split ( " ! " )
except ValueError :
raise ValueError (
" ❌ PROXMOX_TOKEN_NAME must be in the format ' user@realm!tokenid ' "
)
log_event ( f " [Proxmox] Connecting to { host } : { port } as { user } with token ' { token_name } ' " )
return ProxmoxAPI (
host ,
port = int ( port ) ,
user = user ,
token_name = token_name ,
token_value = PROXMOX_TOKEN_VALUE ,
verify_ssl = PROXMOX_VERIFY_SSL
)
# =====================
# JFA-GO HELPERS
# =====================
def refresh_jfa_token ( ) - > bool :
"""
Authenticate to JFA-Go with username/password (Basic auth) against /token/login,
write the returned token to .env (JFA_TOKEN and JFA_API_KEY), and reload env.
Returns True on success.
"""
global JFA_TOKEN , JFA_API_KEY
if not ( JFA_URL and JFA_USERNAME and JFA_PASSWORD ) :
print ( " [JFA] Missing JFA_URL/JFA_USERNAME/JFA_PASSWORD in environment. " )
return False
url = JFA_URL . rstrip ( " / " ) + " /token/login "
headers = { " accept " : " application/json " }
try :
# Option A: let requests build the Basic header
r = requests . get ( url , auth = ( JFA_USERNAME , JFA_PASSWORD ) , headers = headers , timeout = 10 )
# If you prefer to build the header manually (exactly like your curl), use:
# creds = f"{JFA_USERNAME}:{JFA_PASSWORD}".encode()
# b64 = base64.b64encode(creds).decode()
# headers["Authorization"] = f"Basic {b64}"
# r = requests.get(url, headers=headers, timeout=10)
if r . status_code != 200 :
print ( f " [JFA] token login failed: { r . status_code } - { r . text } " )
return False
data = r . json ( ) if r . text else { }
# try common token fields
token = (
data . get ( " token " )
or data . get ( " access_token " )
or data . get ( " jwt " )
or data . get ( " api_key " )
or data . get ( " data " ) # sometimes nested
)
# If API returns {"token": "<token>"} -> good. If it returns a wrapped structure,
# try to handle a couple of other shapes:
if not token :
# if response is {'success': True} or {'invites':...} then no token present
# print for debugging
print ( " [JFA] token not found in response JSON: " , data )
return False
# Persist token to .env under both names (compatibility)
_update_env_key ( " JFA_TOKEN " , token )
_update_env_key ( " JFA_API_KEY " , token )
# Update in-memory values and reload env
JFA_TOKEN = token
JFA_API_KEY = token
load_dotenv ( override = True )
print ( " [JFA] Successfully refreshed token and updated .env " )
return True
except Exception as e :
print ( f " [JFA] Exception while refreshing token: { e } " , exc_info = True )
return False
# =====================
# =====================
# DISCORD HELPERS
# DISCORD HELPERS
# =====================
# =====================
@@ -402,32 +548,26 @@ def get_metadata(key):
cur . close ( )
cur . close ( )
conn . close ( )
conn . close ( )
return row [ 0 ] if row else None
return row [ 0 ] if row else None
def _update_env_key ( key : str , value : str , env_path : str = " .env " ) :
""" Update or append key=value in .env (keeps file order). """
lines = [ ]
found = False
try :
with open ( env_path , " r " , encoding = " utf-8 " ) as f :
lines = f . readlines ( )
except FileNotFoundError :
lines = [ ]
def create_trial_jellyfin_user ( username , password ) :
with open ( env_path , " w " , encoding = " utf-8 " ) as f :
payload = {
for line in lines :
" Name " : username ,
if line . strip ( ) . startswith ( f " { key } = " ) :
" Password " : password ,
f . write ( f " { key } = { value } \n " )
" Policy " : {
found = True
" EnableDownloads " : False ,
else :
" EnableSyncTranscoding " : Fals e,
f . write ( lin e )
" EnableRemoteControlOfOtherUsers " : False ,
if not found :
" EnableLiveTvAccess " : F als e,
f . write ( f " { key } = { v alu e} \n " )
" IsAdministrator " : False ,
" IsHidden " : False ,
" IsDisabled " : False
}
}
headers = {
" X-Emby-Token " : JELLYFIN_API_KEY ,
" Content-Type " : " application/json "
}
response = requests . post ( f " { JELLYFIN_URL } /Users/New " , json = payload , headers = headers )
if response . status_code == 200 :
return response . json ( ) . get ( " Id " )
else :
print ( f " [Jellyfin] Trial user creation failed. Status: { response . status_code } , Response: { response . text } " )
return None
# =====================
# =====================
@@ -752,6 +892,27 @@ async def clearinvites(ctx):
print ( f " [clearinvites] Error: { e } " , exc_info = True )
print ( f " [clearinvites] Error: { e } " , exc_info = True )
@bot.command ( )
async def refreshjfakey ( ctx ) :
""" Admin-only: Force refresh the JFA-Go API key using username/password auth. """
if not has_admin_role ( ctx . author ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
return
if not ENABLE_JFA :
await ctx . send ( " ⚠️ JFA-Go integration is disabled in the configuration. " )
return
await ctx . send ( " 🔁 Attempting to refresh JFA token... " )
success = refresh_jfa_token ( )
if success :
await ctx . send ( " ✅ Successfully refreshed the JFA-Go API token and updated `.env` " )
log_event ( f " Admin { ctx . author } forced a JFA API Token refresh " )
else :
await ctx . send ( " ❌ Failed to refresh the JFA-Go API token. Check bot logs for details. " )
log_event ( f " Admin { ctx . author } attempted JFA API Token refresh but failed " )
@bot.command ( )
@bot.command ( )
async def trialaccount ( ctx , username : str = None , password : str = None ) :
async def trialaccount ( ctx , username : str = None , password : str = None ) :
""" Create a 24-hour trial Jellyfin account. DM-only, one-time per user. """
""" Create a 24-hour trial Jellyfin account. DM-only, one-time per user. """
@@ -888,8 +1049,9 @@ async def deleteaccount(ctx, username: str = None):
await ctx . send ( f " ❌ Failed to delete Jellyfin account ** { username } **. " )
await ctx . send ( f " ❌ Failed to delete Jellyfin account ** { username } **. " )
@bot.command ( )
@bot.command ( )
async def what 2watch( ctx ) :
async def movies 2watch( ctx ) :
""" Pick 5 random movies from the Jellyfin library with embeds and posters. """
log_event ( f " movies2watch invoked by { ctx . author } " )
""" Pick 5 random movies from the Jellyfin library with embeds, and IMDb links. """
member = ctx . guild . get_member ( ctx . author . id ) if ctx . guild else None
member = ctx . guild . get_member ( ctx . author . id ) if ctx . guild else None
if not member or not has_required_role ( member ) :
if not member or not has_required_role ( member ) :
await ctx . send ( f " ❌ { ctx . author . mention } , you don’ t have the required role to use this command. " )
await ctx . send ( f " ❌ { ctx . author . mention } , you don’ t have the required role to use this command. " )
@@ -897,8 +1059,17 @@ async def what2watch(ctx):
headers = { " X-Emby-Token " : JELLYFIN_API_KEY }
headers = { " X-Emby-Token " : JELLYFIN_API_KEY }
try :
try :
# Fetch all movies
# Fetch all movies (include ProviderIds explicitly!)
r = requests . get ( f " { JELLYFIN_URL } /Items?IncludeItemTypes=Movie&Recursive=true " , headers = headers , timeout = 10 )
r = requests . get (
f " { JELLYFIN_URL } /Items " ,
headers = headers ,
params = {
" IncludeItemTypes " : " Movie " ,
" Recursive " : " true " ,
" Fields " : " ProviderIds "
} ,
timeout = 10
)
if r . status_code != 200 :
if r . status_code != 200 :
await ctx . send ( f " ❌ Failed to fetch movies. Status code: { r . status_code } " )
await ctx . send ( f " ❌ Failed to fetch movies. Status code: { r . status_code } " )
return
return
@@ -918,21 +1089,26 @@ async def what2watch(ctx):
)
)
for movie in selection :
for movie in selection :
name = movie . get ( " Name " )
name = movie . get ( " Name " , " Unknown Title " )
year = movie . get ( " ProductionYear " , " N/A " )
year = movie . get ( " ProductionYear " , " N/A " )
runtime = movie . get ( " RunTimeTicks " , None )
runtime = movie . get ( " RunTimeTicks " , None )
runtime_min = int ( runtime / 10_000_000 / 60 ) if runtime else " N/A "
runtime_min = int ( runtime / 10_000_000 / 60 ) if runtime else " N/A "
# Poster URL if available
# Poster URL if available
poster_url = None
poster_url = None
if " PrimaryImageTag " in movie and movie [ " PrimaryImageTag " ] :
if " PrimaryImageTag " in movie and movie [ " PrimaryImageTag " ] :
poster_url = f " { JELLYFIN_URL } /Items/ { movie [ ' Id ' ] } /Images/Primary?tag= { movie [ ' PrimaryImageTag ' ] } &quality=90 "
poster_url = f " { JELLYFIN_URL } /Items/ { movie [ ' Id ' ] } /Images/Primary?tag= { movie [ ' PrimaryImageTag ' ] } &quality=90 "
field_value = f " Year: { year } \n Runtime: { runtime_min } min "
# IMDb link if available
imdb_id = movie . get ( " ProviderIds " , { } ) . get ( " Imdb " )
imdb_link = f " [IMDb Link](https://www.imdb.com/title/ { imdb_id } ) " if imdb_id else " No IMDb ID available "
# Field content
field_value = f " Year: { year } \n Runtime: { runtime_min } min \n { imdb_link } "
embed . add_field ( name = name , value = field_value , inline = False )
embed . add_field ( name = name , value = field_value , inline = False )
if poster_url :
if poster_url :
embed . set_image ( url = poster_url ) # Only last movie's poster will appear as main embed image
embed . set_image ( url = poster_url ) # Only the last poster appears as main embed image
await ctx . send ( embed = embed )
await ctx . send ( embed = embed )
@@ -940,6 +1116,72 @@ async def what2watch(ctx):
await ctx . send ( f " ❌ Error fetching movies: { e } " )
await ctx . send ( f " ❌ Error fetching movies: { e } " )
print ( f " [what2watch] Error: { e } " )
print ( f " [what2watch] Error: { e } " )
@bot.command ( )
async def shows2watch ( ctx ) :
log_event ( f " shows2watch invoked by { ctx . author } " )
""" Pick 5 random TV shows from the Jellyfin library with embeds, and IMDb links. """
member = ctx . guild . get_member ( ctx . author . id ) if ctx . guild else None
if not member or not has_required_role ( member ) :
await ctx . send ( f " ❌ { ctx . author . mention } , you don’ t have the required role to use this command. " )
return
headers = { " X-Emby-Token " : JELLYFIN_API_KEY }
try :
# Fetch all shows (include ProviderIds explicitly!)
r = requests . get (
f " { JELLYFIN_URL } /Items " ,
headers = headers ,
params = {
" IncludeItemTypes " : " Series " ,
" Recursive " : " true " ,
" Fields " : " ProviderIds "
} ,
timeout = 10
)
if r . status_code != 200 :
await ctx . send ( f " ❌ Failed to fetch shows. Status code: { r . status_code } " )
return
shows = r . json ( ) . get ( " Items " , [ ] )
if not shows :
await ctx . send ( " ⚠️ No shows found in the library. " )
return
# Pick 5 random shows
selection = random . sample ( shows , min ( 5 , len ( shows ) ) )
embed = discord . Embed (
title = " 📺 Shows to Watch " ,
description = " Here are 5 random TV show suggestions from the library: " ,
color = discord . Color . green ( )
)
for show in selection :
name = show . get ( " Name " , " Unknown Title " )
year = show . get ( " ProductionYear " , " N/A " )
# Poster URL if available
poster_url = None
if " PrimaryImageTag " in show and show [ " PrimaryImageTag " ] :
poster_url = f " { JELLYFIN_URL } /Items/ { show [ ' Id ' ] } /Images/Primary?tag= { show [ ' PrimaryImageTag ' ] } &quality=90 "
# IMDb link if available
imdb_id = show . get ( " ProviderIds " , { } ) . get ( " Imdb " )
imdb_link = f " [IMDb Link](https://www.imdb.com/title/ { imdb_id } ) " if imdb_id else " No IMDb ID available "
# Field content
field_value = f " Year: { year } \n { imdb_link } "
embed . add_field ( name = name , value = field_value , inline = False )
if poster_url :
embed . set_image ( url = poster_url ) # Only the last show's poster will appear as the embed image
await ctx . send ( embed = embed )
except Exception as e :
await ctx . send ( f " ❌ Error fetching shows: { e } " )
print ( f " [shows2watch] Error: { e } " )
@bot.command ( )
@bot.command ( )
async def cleanup ( ctx ) :
async def cleanup ( ctx ) :
@@ -1225,6 +1467,71 @@ async def qbview(ctx):
await ctx . send ( embed = embed )
await ctx . send ( embed = embed )
@bot.command ( )
async def storage ( ctx ) :
""" Check Proxmox storage pools and ZFS pools. """
if not ENABLE_PROXMOX :
await ctx . send ( " ⚠️ Proxmox integration is disabled in the configuration. " )
return
if not has_admin_role ( ctx . author ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
return
try :
proxmox = get_proxmox_client ( )
embed = discord . Embed (
title = " 📦 Proxmox Storage " ,
description = " Storage pool usage and ZFS pools " ,
color = discord . Color . green ( )
)
for node in proxmox . nodes . get ( ) :
node_name = node [ " node " ]
# ---- ZFS ----
try :
zfs_pools = proxmox . nodes ( node_name ) . disks . zfs . get ( )
if zfs_pools :
zfs_info = [
f " ** { p [ ' name ' ] } **: { p [ ' alloc ' ] / 1024 * * 3 : .2f } GiB / "
f " { p [ ' size ' ] / 1024 * * 3 : .2f } GiB ( { ( p [ ' alloc ' ] / p [ ' size ' ] * 100 ) : .1f } %) "
for p in zfs_pools
]
else :
zfs_info = [ " No ZFS pools found " ]
except Exception as e :
zfs_info = [ f " ⚠️ Failed to fetch ZFS pools ( { e } ) " ]
embed . add_field (
name = f " 🖥️ { node_name } - ZFS Pools " ,
value = " \n " . join ( zfs_info ) ,
inline = False
)
# ---- Normal storage (skip ZFS) ----
try :
storage_info = proxmox . nodes ( node_name ) . storage . get ( )
normal_lines = [
f " ** { s [ ' storage ' ] } **: { s [ ' used ' ] / 1024 * * 3 : .2f } GiB / "
f " { s [ ' total ' ] / 1024 * * 3 : .2f } GiB ( { ( s [ ' used ' ] / s [ ' total ' ] * 100 ) : .1f } %) "
for s in storage_info
if s . get ( " type " , " " ) . lower ( ) not in ( " zfspool " , " zfs " )
]
except Exception as e :
normal_lines = [ f " ⚠️ Failed to fetch normal storage ( { e } ) " ]
embed . add_field (
name = f " 🗳️ { node_name } - Normal Storage " ,
value = " \n " . join ( normal_lines ) or " No non‑ ZFS storage found " ,
inline = False
)
await ctx . send ( embed = embed )
except Exception as e :
await ctx . send ( f " ⚠️ Unexpected error: { e } " )
@bot.command ( )
@bot.command ( )
async def link ( ctx , jellyfin_username : str = None , user : discord . User = None , js_id : str = None ) :
async def link ( ctx , jellyfin_username : str = None , user : discord . User = None , js_id : str = None ) :
log_event ( f " link invoked by { ctx . author } " )
log_event ( f " link invoked by { ctx . author } " )
@@ -1365,7 +1672,8 @@ async def help_command(ctx):
f " ` { PREFIX } createaccount <username> <password>` - Create your Jellyfin account " ,
f " ` { PREFIX } createaccount <username> <password>` - Create your Jellyfin account " ,
f " ` { PREFIX } recoveraccount <newpassword>` - Reset your password " ,
f " ` { PREFIX } recoveraccount <newpassword>` - Reset your password " ,
f " ` { PREFIX } deleteaccount <username>` - Delete your Jellyfin account " ,
f " ` { PREFIX } deleteaccount <username>` - Delete your Jellyfin account " ,
f " ` { PREFIX } what 2watch` - Lists 5 random movie suggestions from the Jellyfin Library"
f " ` { PREFIX } movies 2watch` - Lists 5 random movie suggestions from the Jellyfin Library" ,
f " ` { PREFIX } shows2watch` - Lists 5 random show suggestions from the Jellyfin Library "
]
]
if ENABLE_TRIAL_ACCOUNTS :
if ENABLE_TRIAL_ACCOUNTS :
user_cmds . append ( f " ` { PREFIX } trialaccount <username> <password>` - Create a 24-hour trial Jellyfin account " )
user_cmds . append ( f " ` { PREFIX } trialaccount <username> <password>` - Create a 24-hour trial Jellyfin account " )
@@ -1404,13 +1712,21 @@ async def help_command(ctx):
f " ` { PREFIX } qbview` - Show current qBittorrent downloads with progress, peers, and seeders " ,
f " ` { PREFIX } qbview` - Show current qBittorrent downloads with progress, peers, and seeders " ,
]
]
embed . add_field ( name = " 💾 qBittorrent Commands " , value = " \n " . join ( qb_cmds ) , inline = False )
embed . add_field ( name = " 💾 qBittorrent Commands " , value = " \n " . join ( qb_cmds ) , inline = False )
# --- Proxmox Commands ---
if ENABLE_PROXMOX :
qb_cmds = [
f " ` { PREFIX } storage` - Show available storage pools and free space " ,
]
embed . add_field ( name = " 🗳️ Proxmox Commands " , value = " \n " . join ( qb_cmds ) , inline = False )
# --- JFA Commands ---
# --- JFA Commands ---
if ENABLE_JFA :
if ENABLE_JFA :
jfa_cmds = [
jfa_cmds = [
f " ` { PREFIX } createinvite` - Create a new JFA invite link " ,
f " ` { PREFIX } createinvite` - Create a new JFA invite link " ,
f " ` { PREFIX } listinvites` - List all active JFA invite links " ,
f " ` { PREFIX } listinvites` - List all active JFA invite links " ,
f " ` { PREFIX } deleteinvite <code>` - Delete a specific JFA invite "
f " ` { PREFIX } deleteinvite <code>` - Delete a specific JFA invite " ,
f " ` { PREFIX } refreshjfakey` - Refreshes the JFA API Key Forcefully "
]
]
embed . add_field ( name = " 🔑 JFA Commands " , value = " \n " . join ( jfa_cmds ) , inline = False )
embed . add_field ( name = " 🔑 JFA Commands " , value = " \n " . join ( jfa_cmds ) , inline = False )
@@ -1562,6 +1878,30 @@ async def cleanup_task():
except Exception as e :
except Exception as e :
print ( f " [Cleanup] Failed to send removed message to sync channel: { e } " )
print ( f " [Cleanup] Failed to send removed message to sync channel: { e } " )
# =====================
# JFA-Go Scheduled Token Refresh
# =====================
if ENABLE_JFA :
@tasks.loop ( hours = 1 )
async def refresh_jfa_loop ( ) :
success = refresh_jfa_token ( )
if success :
log_event ( " [JFA] Successfully refreshed token (scheduled loop). " )
else :
log_event ( " [JFA] Failed to refresh token (scheduled loop). " )
@refresh_jfa_loop.before_loop
async def before_refresh_jfa_loop ( ) :
await bot . wait_until_ready ( )
log_event ( " [JFA] Token refresh loop waiting until bot is ready. " )
# Start the loop inside on_ready to ensure event loop exists
@bot.event
async def on_ready ( ) :
if not refresh_jfa_loop . is_running ( ) :
refresh_jfa_loop . start ( )
log_event ( f " Bot is ready. Logged in as { bot . user } " )
@tasks.loop ( hours = 1 )
@tasks.loop ( hours = 1 )
async def check_for_updates ( ) :
async def check_for_updates ( ) :
@@ -1607,6 +1947,9 @@ async def on_ready():
if not cleanup_task . is_running ( ) :
if not cleanup_task . is_running ( ) :
cleanup_task . start ( )
cleanup_task . start ( )
if not refresh_jfa_loop . is_running ( ) :
refresh_jfa_loop . start ( )
if not check_for_updates . is_running ( ) :
if not check_for_updates . is_running ( ) :
check_for_updates . start ( )
check_for_updates . start ( )