Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2624f1ec06 | |||
| bfb16c503a | |||
| 9acbbe5644 | |||
| 2d2e649a5f | |||
| a6f110c9c8 | |||
| 2621fa45e5 | |||
| f1acaf529a | |||
| d970f37343 | |||
| 0ef8fe58d2 | |||
| 24190a3b4a | |||
| 4de1e6e7cb |
3
.env
3
.env
@@ -1,13 +1,14 @@
|
||||
# Discord
|
||||
DISCORD_TOKEN=your_discord_bot_token
|
||||
PREFIX=!
|
||||
GUILD_ID=123456789012345678
|
||||
GUILD_ID=123456789012345678,123456789012345678
|
||||
ADMIN_ROLE_IDS=111111111111111111,222222222222222222
|
||||
REQUIRED_ROLE_IDS=333333333333333333,444444444444444444
|
||||
|
||||
# Jellyfin
|
||||
JELLYFIN_URL=http://127.0.0.1:8096
|
||||
JELLYFIN_API_KEY=your_jellyfin_api_key
|
||||
ENABLE_TRIAL_ACCOUNTS=false
|
||||
|
||||
# Jellyseerr
|
||||
JELLYSEERR_ENABLED=false
|
||||
|
||||
27
CHANGELOG.md
Normal file
27
CHANGELOG.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# 1.0.4
|
||||
|
||||
- Multi-Guild support (As long as a user has a required role or admin role in one server, they are able to use the bot and Jellyfin)
|
||||
- Fixed link command instructions not updating based on Jellyseerr availability
|
||||
|
||||
# 1.0.3
|
||||
|
||||
- Fixed: `ValueError: too many values to unpack (expected 2)`
|
||||
- Cleanup will now delete Jellyseerr accounts as well
|
||||
- Added Trial Jellyfin account support (enable in .env). Will not create a Jellyseerr account, lasts 24 hours, one time use.
|
||||
|
||||
# 1.0.2
|
||||
|
||||
- Fixed Jellyseerr support breaking linking, unlinking, and deletion
|
||||
- Added the ability to link Jellyseerr accounts when linking Jellyfin
|
||||
- Added event logging support for console (Can be toggled in .env or with commands)
|
||||
- Reformatted the updates message
|
||||
- Running a command without any values will now show you the proper command usage
|
||||
|
||||
# 1.0.1
|
||||
|
||||
- Added Jellyseerr Support
|
||||
|
||||
# 1.0.0
|
||||
|
||||
- Bot can now track update releases
|
||||
- Restrict prefixes to non-alphanumeric symbols
|
||||
21
README.md
21
README.md
@@ -1,4 +1,10 @@
|
||||
# Jellyfin-Discord
|
||||
# Jellycord
|
||||
|
||||

|
||||
|
||||
[](https://discord.gg/EdPJAhrDq8)
|
||||

|
||||
|
||||
Allow the creation and management of Jellyfin users via Discord
|
||||
|
||||
Join my [Discord](https://discord.com/invite/zJMUNCPtPy) for help, and keeping an eye out for updates!
|
||||
@@ -7,7 +13,7 @@ This is a very simple and lightweight Jellyfin Discord bot for managing users. I
|
||||
|
||||
Fill out values in the .env and you're good to go!
|
||||
|
||||
# Features
|
||||
## Features
|
||||
|
||||
- Automatic Account Cleanup
|
||||
- Creating Accounts
|
||||
@@ -18,30 +24,31 @@ Fill out values in the .env and you're good to go!
|
||||
- Change bot prefix live
|
||||
- Checks for new releases
|
||||
|
||||
# Command Overview
|
||||
## Command Overview
|
||||
|
||||
**Pinging the bot will show you the necessary commands to create your account.**
|
||||
|
||||
**PLEASE NOTE BEFORE USING. THIS BOT IS MEANT TO USE REQUIRED ROLES IN ORDER TO WHITELIST USERS FOR JELLYFIN. TAKING A USERS ROLE AWAY WILL DELETE THEIR JELLYFIN ACCOUNT WHEN THE BOT RUNS ITS CLEANUP (24 Hour Schedule or Admin Forced)**
|
||||
|
||||

|
||||

|
||||
|
||||
**There are protections in place to stop users from creating an account where people can see. If a user sends the account creation or reset in a guild, the bot will delete it.**
|
||||
|
||||

|
||||

|
||||
|
||||
**If a user already has a linked Jellyfin account, the bot will not allow them to create another account.**
|
||||
|
||||

|
||||

|
||||
|
||||
**In order to create an account, you must have the required roles specified in the .env**
|
||||
|
||||

|
||||

|
||||
|
||||
***User Commands***
|
||||
- `!createaccount` <username> <password> - Create your Jellyfin account
|
||||
- `!recoveraccount` <username> <newpassword> - Reset your password
|
||||
- `!deleteaccount` <username> - Delete your Jellyfin account
|
||||
- `!trialaccount` <username> <password> - Create a 24-hour trial Jellyfin account. Only if ENABLE_TRIAL_ACCOUNTS=True
|
||||
|
||||
***Admin Commands***
|
||||
- `!cleanup` - Remove Jellyfin accounts from users without roles
|
||||
|
||||
441
app.py
441
app.py
@@ -22,13 +22,14 @@ def get_env_var(key: str, cast=str, required=True):
|
||||
|
||||
TOKEN = get_env_var("DISCORD_TOKEN")
|
||||
PREFIX = os.getenv("PREFIX", "!") # Default to "!" if not set
|
||||
GUILD_ID = get_env_var("GUILD_ID", int)
|
||||
GUILD_IDS = [int(x.strip()) for x in get_env_var("GUILD_IDS").split(",")]
|
||||
REQUIRED_ROLE_IDS = [int(x) for x in get_env_var("REQUIRED_ROLE_IDS").split(",")]
|
||||
ADMIN_ROLE_IDS = [int(x) for x in get_env_var("ADMIN_ROLE_IDS").split(",")]
|
||||
SYNC_LOG_CHANNEL_ID = get_env_var("SYNC_LOG_CHANNEL_ID", int)
|
||||
|
||||
JELLYFIN_URL = get_env_var("JELLYFIN_URL")
|
||||
JELLYFIN_API_KEY = get_env_var("JELLYFIN_API_KEY")
|
||||
ENABLE_TRIAL_ACCOUNTS = os.getenv("ENABLE_TRIAL_ACCOUNTS", "False").lower() == "true"
|
||||
|
||||
JELLYSEERR_ENABLED = os.getenv("JELLYSEERR_ENABLED", "false").lower() == "true"
|
||||
JELLYSEERR_URL = os.getenv("JELLYSEERR_URL", "").rstrip("/")
|
||||
@@ -39,9 +40,9 @@ DB_USER = get_env_var("DB_USER")
|
||||
DB_PASSWORD = get_env_var("DB_PASSWORD")
|
||||
DB_NAME = get_env_var("DB_NAME")
|
||||
|
||||
BOT_VERSION = "1.0.2"
|
||||
VERSION_URL = "https://raw.githubusercontent.com/PenguCCN/Jellyfin-Discord/main/version.txt"
|
||||
RELEASES_URL = "https://github.com/PenguCCN/Jellyfin-Discord/releases"
|
||||
BOT_VERSION = "1.0.4"
|
||||
VERSION_URL = "https://raw.githubusercontent.com/PenguCCN/Jellycord/main/version.txt"
|
||||
RELEASES_URL = "https://github.com/PenguCCN/Jellycord/releases"
|
||||
|
||||
# =====================
|
||||
# EVENT LOGGING
|
||||
@@ -65,25 +66,27 @@ bot = commands.Bot(command_prefix=PREFIX, intents=intents, help_command=None)
|
||||
# =====================
|
||||
def init_db():
|
||||
log_event(f"Initiating Database...")
|
||||
# Create database if it doesn't exist
|
||||
conn = mysql.connector.connect(host=DB_HOST, user=DB_USER, password=DB_PASSWORD)
|
||||
conn = mysql.connector.connect(
|
||||
host=DB_HOST, user=DB_USER, password=DB_PASSWORD
|
||||
)
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"CREATE DATABASE IF NOT EXISTS `{DB_NAME}`")
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# Connect to the database
|
||||
conn = mysql.connector.connect(host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME)
|
||||
conn = mysql.connector.connect(
|
||||
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
|
||||
)
|
||||
cur = conn.cursor()
|
||||
|
||||
# Create accounts table if it doesn't exist
|
||||
# Normal accounts table
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
discord_id BIGINT PRIMARY KEY,
|
||||
jellyfin_username VARCHAR(255) NOT NULL,
|
||||
jellyfin_id VARCHAR(255) NOT NULL,
|
||||
jellyseerr_id VARCHAR(255) DEFAULT NULL
|
||||
jellyseerr_id VARCHAR(255)
|
||||
)
|
||||
""")
|
||||
|
||||
@@ -99,7 +102,18 @@ def init_db():
|
||||
cur.execute("ALTER TABLE accounts ADD COLUMN jellyseerr_id VARCHAR(255) DEFAULT NULL")
|
||||
print("[DB] Added missing column 'jellyseerr_id' to accounts table.")
|
||||
|
||||
# Create bot_metadata table if it doesn't exist
|
||||
# Trial accounts table (persistent history, one-time only)
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS trial_accounts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
discord_id BIGINT NOT NULL UNIQUE,
|
||||
jellyfin_username VARCHAR(255),
|
||||
jellyfin_id VARCHAR(255),
|
||||
trial_created_at DATETIME NOT NULL,
|
||||
expired BOOLEAN DEFAULT 0
|
||||
)
|
||||
""")
|
||||
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS bot_metadata (
|
||||
key_name VARCHAR(255) PRIMARY KEY,
|
||||
@@ -107,6 +121,14 @@ def init_db():
|
||||
)
|
||||
""")
|
||||
|
||||
# Cleanup logs table
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS cleanup_logs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
run_at DATETIME NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
@@ -125,6 +147,26 @@ def add_account(discord_id, username, jf_id, js_id=None):
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
def init_trial_accounts_table():
|
||||
conn = mysql.connector.connect(
|
||||
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
|
||||
)
|
||||
cur = conn.cursor()
|
||||
# Persistent trial accounts table
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS trial_accounts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
discord_id BIGINT NOT NULL UNIQUE,
|
||||
jellyfin_username VARCHAR(255) NOT NULL,
|
||||
jellyfin_id VARCHAR(255) NOT NULL,
|
||||
trial_created_at DATETIME NOT NULL,
|
||||
expired BOOLEAN DEFAULT 0
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_accounts():
|
||||
conn = mysql.connector.connect(
|
||||
@@ -269,11 +311,30 @@ def delete_jellyseerr_user(js_id: str) -> bool:
|
||||
# =====================
|
||||
# DISCORD HELPERS
|
||||
# =====================
|
||||
def has_required_role(member):
|
||||
return any(role.id in REQUIRED_ROLE_IDS for role in member.roles)
|
||||
|
||||
def has_admin_role(member):
|
||||
return any(role.id in ADMIN_ROLE_IDS for role in member.roles)
|
||||
def has_required_role(user: discord.User | discord.Member) -> bool:
|
||||
"""Check if the user has any of the required roles across all configured guilds."""
|
||||
for gid in GUILD_IDS:
|
||||
guild = bot.get_guild(gid)
|
||||
if not guild:
|
||||
continue
|
||||
member = guild.get_member(user.id)
|
||||
if member and any(role.id in REQUIRED_ROLE_IDS for role in member.roles):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def has_admin_role(user: discord.User | discord.Member) -> bool:
|
||||
"""Check if the user has any of the admin roles across all configured guilds."""
|
||||
for gid in GUILD_IDS:
|
||||
guild = bot.get_guild(gid)
|
||||
if not guild:
|
||||
continue
|
||||
member = guild.get_member(user.id)
|
||||
if member and any(role.id in ADMIN_ROLE_IDS for role in member.roles):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# =====================
|
||||
# BOT HELPERS
|
||||
@@ -302,6 +363,33 @@ def get_metadata(key):
|
||||
conn.close()
|
||||
return row[0] if row else None
|
||||
|
||||
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
|
||||
|
||||
|
||||
# =====================
|
||||
# EVENTS
|
||||
# =====================
|
||||
@@ -349,8 +437,14 @@ async def createaccount(ctx, username: str = None, password: str = None):
|
||||
await ctx.send(f"{ctx.author.mention} ❌ Please DM me to create your Jellyfin account.")
|
||||
return
|
||||
|
||||
guild = bot.get_guild(GUILD_ID)
|
||||
member = guild.get_member(ctx.author.id) if guild else None
|
||||
member = None
|
||||
for gid in GUILD_IDS:
|
||||
guild = bot.get_guild(gid)
|
||||
if guild:
|
||||
member = guild.get_member(ctx.author.id)
|
||||
if member and has_required_role(member):
|
||||
break
|
||||
|
||||
if not member or not has_required_role(member):
|
||||
await ctx.send(f"❌ {ctx.author.mention}, you don’t have the required role.")
|
||||
return
|
||||
@@ -381,6 +475,83 @@ async def createaccount(ctx, username: str = None, password: str = None):
|
||||
else:
|
||||
await ctx.send(f"❌ Failed to create Jellyfin account **{username}**. It may already exist.")
|
||||
|
||||
@bot.command()
|
||||
async def trialaccount(ctx, username: str = None, password: str = None):
|
||||
"""Create a 24-hour trial Jellyfin account. DM-only, one-time per user."""
|
||||
log_event(f"trialaccount invoked by {ctx.author}")
|
||||
|
||||
# Ensure trial accounts are enabled
|
||||
if not ENABLE_TRIAL_ACCOUNTS:
|
||||
await ctx.send("❌ Trial accounts are currently disabled.")
|
||||
return
|
||||
|
||||
# Ensure it's a DM
|
||||
if not isinstance(ctx.channel, discord.DMChannel):
|
||||
try:
|
||||
await ctx.message.delete()
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
await ctx.send(f"{ctx.author.mention} ❌ Please DM me to create a trial account.")
|
||||
return
|
||||
|
||||
# Ensure required arguments
|
||||
if username is None or password is None:
|
||||
await ctx.send(command_usage(f"{PREFIX}trialaccount", ["<username>", "<password>"]))
|
||||
return
|
||||
|
||||
member = None
|
||||
for gid in GUILD_IDS:
|
||||
guild = bot.get_guild(gid)
|
||||
if guild:
|
||||
member = guild.get_member(ctx.author.id)
|
||||
if member and has_required_role(member):
|
||||
break
|
||||
|
||||
if not member or not has_required_role(member):
|
||||
await ctx.send(f"❌ {ctx.author.mention}, you don’t have the required role.")
|
||||
return
|
||||
|
||||
# Check if user already has a normal Jellyfin account
|
||||
if get_account_by_discord(ctx.author.id):
|
||||
await ctx.send(f"❌ {ctx.author.mention}, you already have a Jellyfin account.")
|
||||
return
|
||||
|
||||
# Check if user already had a trial account (one-time)
|
||||
conn = mysql.connector.connect(
|
||||
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
|
||||
)
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM trial_accounts WHERE discord_id=%s", (ctx.author.id,))
|
||||
existing_trial = cur.fetchone()
|
||||
if existing_trial:
|
||||
cur.close()
|
||||
conn.close()
|
||||
await ctx.send(f"❌ {ctx.author.mention}, you have already used your trial account. You cannot create another.")
|
||||
return
|
||||
|
||||
# Create Jellyfin trial user
|
||||
if create_jellyfin_user(username, password):
|
||||
jf_id = get_jellyfin_user(username)
|
||||
if not jf_id:
|
||||
await ctx.send(f"❌ Failed to fetch Jellyfin ID for **{username}**. Please contact an admin.")
|
||||
return
|
||||
|
||||
# Store trial account info in separate persistent table
|
||||
cur.execute("""
|
||||
INSERT INTO trial_accounts (discord_id, jellyfin_username, jellyfin_id, trial_created_at, expired)
|
||||
VALUES (%s, %s, %s, NOW(), 0)
|
||||
""", (ctx.author.id, username, jf_id))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
await ctx.send(f"✅ Trial Jellyfin account **{username}** created! It will expire in 24 hours.\n🌐 Login here: {JELLYFIN_URL}")
|
||||
log_event(f"Trial account created for {ctx.author} ({username})")
|
||||
else:
|
||||
cur.close()
|
||||
conn.close()
|
||||
await ctx.send(f"❌ Failed to create trial account **{username}**. It may already exist.")
|
||||
|
||||
|
||||
@bot.command()
|
||||
async def recoveraccount(ctx, new_password: str = None):
|
||||
@@ -443,13 +614,30 @@ async def deleteaccount(ctx, username: str = None):
|
||||
@bot.command()
|
||||
async def cleanup(ctx):
|
||||
log_event(f"cleanup invoked by {ctx.author}")
|
||||
guild = bot.get_guild(GUILD_ID)
|
||||
removed = []
|
||||
for discord_id, jf_username in get_accounts():
|
||||
m = guild.get_member(discord_id)
|
||||
if m is None or not has_required_role(m):
|
||||
|
||||
for discord_id, jf_username, jf_id, js_id in get_accounts():
|
||||
member = None
|
||||
for gid in GUILD_IDS:
|
||||
guild = bot.get_guild(gid)
|
||||
if guild:
|
||||
member = guild.get_member(discord_id)
|
||||
if member:
|
||||
break
|
||||
|
||||
if member is None or not has_required_role(member):
|
||||
if delete_jellyfin_user(jf_username):
|
||||
delete_account(discord_id)
|
||||
|
||||
if JELLYSEERR_ENABLED and js_id:
|
||||
try:
|
||||
headers = {"X-Api-Key": JELLYSEERR_API_KEY}
|
||||
dr = requests.delete(f"{JELLYSEERR_URL}/api/v1/user/{js_id}", headers=headers, timeout=10)
|
||||
if dr.status_code in (200, 204):
|
||||
print(f"[Jellyseerr] User {js_id} removed successfully.")
|
||||
except Exception as e:
|
||||
print(f"[Jellyseerr] Failed to delete user {js_id}: {e}")
|
||||
|
||||
removed.append(jf_username)
|
||||
|
||||
log_channel = bot.get_channel(SYNC_LOG_CHANNEL_ID)
|
||||
@@ -458,12 +646,59 @@ async def cleanup(ctx):
|
||||
|
||||
await ctx.send("✅ Cleanup complete.")
|
||||
|
||||
@bot.command()
|
||||
async def listvalidusers(ctx):
|
||||
"""Admin-only: List how many registered users have a valid role."""
|
||||
if not has_admin_role(ctx.author):
|
||||
await ctx.send("❌ You don’t have permission to use this command.")
|
||||
return
|
||||
|
||||
accounts = get_accounts()
|
||||
valid_users = []
|
||||
invalid_users = []
|
||||
|
||||
for discord_id, jf_username, jf_id, js_id in accounts:
|
||||
user = await bot.fetch_user(discord_id)
|
||||
if has_required_role(user):
|
||||
valid_users.append(user)
|
||||
else:
|
||||
invalid_users.append(user)
|
||||
|
||||
embed = discord.Embed(
|
||||
title="📊 Registered User Role Status",
|
||||
color=discord.Color.green()
|
||||
)
|
||||
embed.add_field(
|
||||
name="✅ Valid Users",
|
||||
value=f"{len(valid_users)} users",
|
||||
inline=True
|
||||
)
|
||||
embed.add_field(
|
||||
name="❌ Invalid Users",
|
||||
value=f"{len(invalid_users)} users",
|
||||
inline=True
|
||||
)
|
||||
if len(valid_users) > 0:
|
||||
embed.add_field(
|
||||
name="Valid Users List",
|
||||
value="\n".join([u.mention for u in valid_users[:20]]) + ("..." if len(valid_users) > 20 else ""),
|
||||
inline=False
|
||||
)
|
||||
if len(invalid_users) > 0:
|
||||
embed.add_field(
|
||||
name="Invalid Users List",
|
||||
value="\n".join([u.mention for u in invalid_users[:20]]) + ("..." if len(invalid_users) > 20 else ""),
|
||||
inline=False
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
@bot.command()
|
||||
async def lastcleanup(ctx):
|
||||
log_event(f"lastcleanup invoked by {ctx.author}")
|
||||
member = ctx.guild.get_member(ctx.author.id)
|
||||
if not has_admin_role(member):
|
||||
if not has_admin_role(ctx.author):
|
||||
await ctx.send("❌ You don’t have permission to view the last cleanup.")
|
||||
return
|
||||
|
||||
@@ -517,7 +752,7 @@ async def searchdiscord(ctx, user: discord.User = None):
|
||||
async def scanlibraries(ctx):
|
||||
log_event(f"scanlibraries invoked by {ctx.author}")
|
||||
member = ctx.guild.get_member(ctx.author.id)
|
||||
if not has_admin_role(member):
|
||||
if not has_admin_role(ctx.author):
|
||||
await ctx.send("❌ You don’t have permission to use this command.")
|
||||
return
|
||||
|
||||
@@ -576,7 +811,7 @@ async def setprefix(ctx, new_prefix: str = None):
|
||||
return
|
||||
|
||||
member = ctx.guild.get_member(ctx.author.id)
|
||||
if not member or not has_admin_role(member):
|
||||
if not has_admin_role(ctx.author):
|
||||
await ctx.send("❌ You don’t have permission to use this command.")
|
||||
return
|
||||
|
||||
@@ -603,7 +838,7 @@ async def setprefix(ctx, new_prefix: str = None):
|
||||
async def updates(ctx):
|
||||
log_event(f"updates invoked by {ctx.author}")
|
||||
member = ctx.guild.get_member(ctx.author.id)
|
||||
if not has_admin_role(member):
|
||||
if not has_admin_role(ctx.author):
|
||||
await ctx.send("❌ You don’t have permission to use this command.")
|
||||
return
|
||||
|
||||
@@ -621,7 +856,7 @@ async def updates(ctx):
|
||||
async def logging(ctx, state: str):
|
||||
"""Admin-only: Enable or disable event logging."""
|
||||
member = ctx.guild.get_member(ctx.author.id)
|
||||
if not member or not has_admin_role(member):
|
||||
if not has_admin_role(ctx.author):
|
||||
await ctx.send("❌ You don’t have permission to use this command.")
|
||||
return
|
||||
|
||||
@@ -655,7 +890,7 @@ async def logging(ctx, state: str):
|
||||
async def help_command(ctx):
|
||||
log_event(f"Command help invoked by {ctx.author}")
|
||||
member = ctx.guild.get_member(ctx.author.id)
|
||||
is_admin = has_admin_role(member)
|
||||
is_admin = has_admin_role(ctx.author)
|
||||
|
||||
embed = discord.Embed(
|
||||
title=f"📖 Jellyfin Bot Help {BOT_VERSION}",
|
||||
@@ -663,24 +898,39 @@ async def help_command(ctx):
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
|
||||
embed.add_field(name="User Commands", value=(
|
||||
f"`{PREFIX}createaccount <username> <password>` - Create your Jellyfin account\n"
|
||||
f"`{PREFIX}recoveraccount <newpassword>` - Reset your password\n"
|
||||
f"`{PREFIX}deleteaccount <username>` - Delete your Jellyfin account\n"
|
||||
), inline=False)
|
||||
# User commands
|
||||
user_cmds = [
|
||||
f"`{PREFIX}createaccount <username> <password>` - Create your Jellyfin account",
|
||||
f"`{PREFIX}recoveraccount <newpassword>` - Reset your password",
|
||||
f"`{PREFIX}deleteaccount <username>` - Delete your Jellyfin account"
|
||||
]
|
||||
|
||||
# Only show trialaccount if enabled
|
||||
if ENABLE_TRIAL_ACCOUNTS:
|
||||
user_cmds.append(f"`{PREFIX}trialaccount <username> <password>` - Create a 24-hour trial Jellyfin account")
|
||||
|
||||
embed.add_field(name="User Commands", value="\n".join(user_cmds), inline=False)
|
||||
|
||||
# Admin commands
|
||||
if is_admin:
|
||||
# Dynamic link command line
|
||||
link_command = f"`{PREFIX}link <jellyfin_username> @user` - Manually link accounts"
|
||||
if JELLYSEERR_ENABLED:
|
||||
link_command = f"`{PREFIX}link <jellyfin_username> @user <Jellyseerr ID>` - Manually link accounts with Jellyseerr"
|
||||
|
||||
embed.add_field(name="Admin Commands", value=(
|
||||
f"`{PREFIX}cleanup` - Remove Jellyfin accounts from users without roles\n"
|
||||
f"`{PREFIX}listvalidusers` - Show number of valid and invalid accounts\n"
|
||||
f"`{PREFIX}lastcleanup` - See Last cleanup time, and time remaining before next cleanup\n"
|
||||
f"`{PREFIX}searchaccount <jellyfin_username>` - Find linked Discord user\n"
|
||||
f"`{PREFIX}searchdiscord @user` - Find linked Jellyfin account\n"
|
||||
f"`{PREFIX}scanlibraries` - Scan all Jellyfin libraries\n"
|
||||
f"`{PREFIX}link <jellyfin_username> @user` - Manually link accounts\n"
|
||||
f"{link_command}\n"
|
||||
f"`{PREFIX}unlink @user` - Manually unlink accounts\n"
|
||||
), inline=False)
|
||||
|
||||
embed.add_field(name="Admin Bot Commands", value=(
|
||||
f"`{PREFIX}setprefix` - Change the bots command prefix\n"
|
||||
f"`{PREFIX}setprefix` - Change the bot's command prefix\n"
|
||||
f"`{PREFIX}updates` - Manually check for bot updates\n"
|
||||
f"`{PREFIX}logging` - Enable/Disable Console Event Logging\n"
|
||||
), inline=False)
|
||||
@@ -694,22 +944,125 @@ import datetime
|
||||
|
||||
@tasks.loop(hours=24)
|
||||
async def daily_check():
|
||||
guild = bot.get_guild(GUILD_ID)
|
||||
log_event("Running daily account cleanup check...")
|
||||
removed = []
|
||||
|
||||
for discord_id, jf_username in get_accounts():
|
||||
m = guild.get_member(discord_id)
|
||||
if m is None or not has_required_role(m):
|
||||
# Normal accounts cleanup
|
||||
for row in get_accounts():
|
||||
# safe unpacking in case schema varies
|
||||
discord_id = row[0]
|
||||
jf_username = row[1] if len(row) > 1 else None
|
||||
jf_id = row[2] if len(row) > 2 else None
|
||||
js_id = row[3] if len(row) > 3 else None
|
||||
|
||||
# find the member across configured guilds
|
||||
member = None
|
||||
for gid in GUILD_IDS:
|
||||
guild = bot.get_guild(gid)
|
||||
if not guild:
|
||||
continue
|
||||
candidate = guild.get_member(discord_id)
|
||||
if candidate:
|
||||
member = candidate
|
||||
break
|
||||
|
||||
# if no member found or member doesn't have a required role -> delete account
|
||||
if member is None or not has_required_role(member):
|
||||
if jf_username:
|
||||
try:
|
||||
if delete_jellyfin_user(jf_username):
|
||||
log_event(f"Deleted Jellyfin user {jf_username} for Discord ID {discord_id}")
|
||||
else:
|
||||
log_event(f"Failed to delete Jellyfin user {jf_username} for Discord ID {discord_id}")
|
||||
except Exception as e:
|
||||
print(f"[Cleanup] Error deleting Jellyfin user {jf_username}: {e}")
|
||||
|
||||
# remove DB entry for normal account
|
||||
try:
|
||||
delete_account(discord_id)
|
||||
removed.append(jf_username)
|
||||
except Exception as e:
|
||||
print(f"[Cleanup] Error removing DB entry for Discord ID {discord_id}: {e}")
|
||||
|
||||
if removed:
|
||||
print(f"Daily cleanup: removed {len(removed)} accounts: {removed}")
|
||||
# remove from Jellyseerr if we have an id and integration enabled
|
||||
if JELLYSEERR_ENABLED and js_id:
|
||||
try:
|
||||
if delete_jellyseerr_user(js_id):
|
||||
log_event(f"Deleted Jellyseerr user {js_id} for Discord ID {discord_id}")
|
||||
else:
|
||||
log_event(f"Failed to delete Jellyseerr user {js_id} for Discord ID {discord_id}")
|
||||
except Exception as e:
|
||||
print(f"[Cleanup] Failed to delete Jellyseerr user {js_id}: {e}")
|
||||
|
||||
# Log last run timestamp
|
||||
removed.append(jf_username or f"{discord_id}")
|
||||
|
||||
# Trial accounts cleanup (persistent history table)
|
||||
try:
|
||||
conn = mysql.connector.connect(
|
||||
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
|
||||
)
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute("SELECT * FROM trial_accounts WHERE expired=0")
|
||||
trials = cur.fetchall()
|
||||
|
||||
for trial in trials:
|
||||
created_at = trial.get("trial_created_at") or trial.get("created_at") # compatibility
|
||||
if not created_at:
|
||||
continue
|
||||
|
||||
# created_at is a datetime from the DB (cursor dictionary=True)
|
||||
if datetime.datetime.utcnow() > created_at + datetime.timedelta(hours=24):
|
||||
# delete from Jellyfin (best-effort)
|
||||
try:
|
||||
delete_jellyfin_user(trial.get("jellyfin_username"))
|
||||
except Exception as e:
|
||||
print(f"[Trial Cleanup] Error deleting trial Jellyfin user {trial.get('jellyfin_username')}: {e}")
|
||||
|
||||
# mark trial as expired
|
||||
try:
|
||||
cur.execute("UPDATE trial_accounts SET expired=1 WHERE discord_id=%s", (trial["discord_id"],))
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
print(f"[Trial Cleanup] Error marking trial expired for {trial['discord_id']}: {e}")
|
||||
|
||||
removed.append(f"{trial.get('jellyfin_username')} (trial)")
|
||||
except Exception as e:
|
||||
print(f"[Trial Cleanup] Error reading trial accounts: {e}")
|
||||
finally:
|
||||
try:
|
||||
cur.close()
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# record last run in metadata and cleanup_logs
|
||||
try:
|
||||
set_metadata("last_cleanup", datetime.datetime.utcnow().isoformat())
|
||||
log_event(f"Daily cleanup: removed {len(removed)} accounts: {removed}")
|
||||
except Exception as e:
|
||||
print(f"[Cleanup] Failed to set last_cleanup metadata: {e}")
|
||||
|
||||
try:
|
||||
conn = mysql.connector.connect(
|
||||
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
|
||||
)
|
||||
cur = conn.cursor()
|
||||
cur.execute("INSERT INTO cleanup_logs (run_at) VALUES (%s)", (datetime.datetime.utcnow(),))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"[Cleanup] Failed to insert cleanup_logs: {e}")
|
||||
|
||||
# post results to sync channel if anything removed
|
||||
if removed:
|
||||
msg = f"🧹 Removed {len(removed)} Jellyfin accounts: {', '.join(removed)}"
|
||||
print(msg)
|
||||
try:
|
||||
log_channel = bot.get_channel(SYNC_LOG_CHANNEL_ID)
|
||||
if log_channel:
|
||||
await log_channel.send(msg)
|
||||
except Exception as e:
|
||||
print(f"[Cleanup] Failed to send removed message to sync channel: {e}")
|
||||
|
||||
|
||||
@tasks.loop(hours=1)
|
||||
async def check_for_updates():
|
||||
@@ -731,8 +1084,6 @@ async def check_for_updates():
|
||||
print(f"[Update Check] Failed: {e}")
|
||||
|
||||
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
print(f"Logged in as {bot.user}")
|
||||
|
||||
1
version.json
Normal file
1
version.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "version": "1.0.4" }
|
||||
@@ -1 +1 @@
|
||||
1.0.2
|
||||
1.0.4
|
||||
Reference in New Issue
Block a user