From fb105a2927ad2e756d2eacd32cd21eb4ffcd80cd Mon Sep 17 00:00:00 2001 From: Yoruio Date: Sat, 9 Jul 2022 17:43:52 -0600 Subject: [PATCH] feat: Jellyfin integration finished - Finsihed Jellyfin integration with auto-add and auto-remove - Integration toggle in config task: none --- README.md | 26 ++- app/bot/cogs/app.py | 302 +++++++++++++++++++++++++------ app/bot/helper/confighelper.py | 63 ++++++- app/bot/helper/db.py | 28 ++- app/bot/helper/jellyfinhelper.py | 175 ++++++++++++++++++ app/bot/helper/plexhelper.py | 5 +- requirements.txt | 2 + run.py | 153 ++++++++++++++-- 8 files changed, 665 insertions(+), 89 deletions(-) create mode 100644 app/bot/helper/jellyfinhelper.py diff --git a/README.md b/README.md index 7387cdc..21b7777 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,14 @@ Invitarr is a chatbot that invites discord users to plex. You can also automate Commands: ``` -.plexinvite +.plexinvite / .plexadd This command is used to add an email to plex -.plexremove +.plexremove / .plexrm This command is used to remove an email from plex +.jellyfininvite / .jellyadd +This command is used to add a user to Jellyfin. +.jellyremove / .jellyrm +This command is used to remove a user from Jellyfin. .dbls This command is used to list Invitarrs database .dbadd <@user> "" "" @@ -66,7 +70,7 @@ docker run -d --restart unless-stopped --name invitarr -v /path to config:/app/a # After bot has started -# Setup Commands: +# Plex Setup Commands: ``` .setupplex @@ -74,7 +78,21 @@ This command is used to setup plex login. .plexroleadd <@role> These role(s) will be used as the role(s) to automatically invite user to plex .setupplexlibs (optional) -This command is used to setup plex libraries. Default is set to all. +This command is used to setup plex libraries. Default is set to all. +.plexdisable +This command disables the Plex integration (currently only disables auto-add / auto-remove) +``` + +# Jellyfin Setup Commands: +``` +.setupjelly +This command is used to setup Jellyfin API. +.jellyroleadd <@role> +These role(s) will be used as the role(s) to automatically invite user to Jellyfin +.setupjellylibs (optional) +This command is used to setup jelly libraries. Default is set to all. +.jellydisable +this command disables the Jellyfin integration (currently only disables auto-add / auto-remove) ``` Refer to the [Wiki](https://github.com/Sleepingpirates/Invitarr/wiki) for detailed steps. diff --git a/app/bot/cogs/app.py b/app/bot/cogs/app.py index ebd717b..ec09f24 100644 --- a/app/bot/cogs/app.py +++ b/app/bot/cogs/app.py @@ -1,3 +1,5 @@ +from pickle import FALSE +import app.bot.helper.jellyfinhelper as jelly import discord from discord.ext import commands import asyncio @@ -5,6 +7,7 @@ from plexapi.myplex import MyPlexAccount from discord import Webhook, AsyncWebhookAdapter import app.bot.helper.db as db import app.bot.helper.plexhelper as plexhelper +import app.bot.helper.jellyfinhelper as jelly import texttable import os from os import path @@ -19,28 +22,91 @@ PLEXPASS = "" PLEX_SERVER_NAME = "" Plex_LIBS = None -USE_PLEX = False +plex_configured = True +jellyfin_configured = True -if(path.exists('app/config/config.ini')): +if(path.exists(CONFIG_PATH)): + config = configparser.ConfigParser() + config.read(CONFIG_PATH) + + # Get Plex config try: - config = configparser.ConfigParser() - config.read(CONFIG_PATH) PLEXUSER = config.get(BOT_SECTION, 'plex_user') PLEXPASS = config.get(BOT_SECTION, 'plex_pass') PLEX_SERVER_NAME = config.get(BOT_SECTION, 'plex_server_name') except: - pass -if(path.exists('app/config/config.ini')): + print("Could not load plex config") + plex_configured = False + + # Get Plex roles config try: plex_roles = config.get(BOT_SECTION, 'plex_roles') except: - pass -if(path.exists('app/config/config.ini')): + print("Could not get Plex roles config") + plex_roles = None + if plex_roles is not None: + plex_roles = list(plex_roles.split(',')) + else: + plex_roles = [] + + # Get Plex libs config try: Plex_LIBS = config.get(BOT_SECTION, 'plex_libs') except: - pass -if USE_PLEX: + print("Could not get Plex libs config. Defaulting to all libraries.") + Plex_LIBS = None + if Plex_LIBS is None: + Plex_LIBS = ["all"] + else: + Plex_LIBS = list(Plex_LIBS.split(',')) + + # Get Jellyfin config + try: + JELLYFIN_SERVER_URL = config.get(BOT_SECTION, 'jellyfin_server_url') + JELLYFIN_API_KEY = config.get(BOT_SECTION, "jellyfin_api_key") + except: + print("Could not load Jellyfin config") + jellyfin_configured = False + + # Get Jellyfin roles config + try: + jellyfin_roles = config.get(BOT_SECTION, 'jellyfin_roles') + except: + print("Could not get Jellyfin roles config") + jellyfin_roles = None + if jellyfin_roles is not None: + jellyfin_roles = list(jellyfin_roles.split(',')) + else: + jellyfin_roles = [] + + # Get Jellyfin libs config + try: + jellyfin_libs = config.get(BOT_SECTION, 'jellyfin_libs') + except: + print("Could not get Jellyfin libs config. Defaulting to all libraries.") + jellyfin_libs = None + if jellyfin_libs is None: + jellyfin_libs = ["all"] + else: + jellyfin_libs = list(jellyfin_libs.split(',')) + + # Get Enable config + try: + USE_JELLYFIN = config.get(BOT_SECTION, 'jellyfin_enabled') + USE_JELLYFIN = USE_JELLYFIN.lower() == "true" + except: + print("Could not get Jellyfin enable config. Defaulting to False") + USE_JELLYFIN = False + + try: + USE_PLEX = config.get(BOT_SECTION, "plex_enabled") + USE_PLEX = USE_PLEX.lower() == "true" + except: + print("Could not get Plex enable config. Defaulting to False") + USE_PLEX = False + + +if USE_PLEX and plex_configured: try: account = MyPlexAccount(PLEXUSER, PLEXPASS) plex = account.resource(PLEX_SERVER_NAME).connect() # returns a PlexServer instance @@ -48,14 +114,9 @@ if USE_PLEX: except Exception as e: print('Error with plex login. Please check username and password and Plex server name or setup plex in the bot.') print(f'Error: {e}') - -if plex_roles is not None: - plex_roles = list(plex_roles.split(',')) - -if Plex_LIBS is None: - Plex_LIBS = ["all"] else: - Plex_LIBS = list(Plex_LIBS.split(',')) + print(f"Plex {'disabled' if not USE_PLEX else 'not configured'}. Skipping Plex login.") + class app(commands.Cog): @@ -65,6 +126,7 @@ class app(commands.Cog): @commands.Cog.listener() async def on_ready(self): print('Made by Sleepingpirate https://github.com/Sleepingpirates/') + print('Jellyfin implementation by Yoruio https://github.com/Yoruio/') print(f'Logged in as {self.bot.user} (ID: {self.bot.user.id})') print('------') if plex_roles is None: @@ -77,6 +139,12 @@ class app(commands.Cog): async def embedinfo(self, author, message): embed1 = discord.Embed(title=message, color=0x00F500) await author.send(embed=embed1) + + async def embedcustom(self, recipient, title, fields): + embed = discord.Embed(title=title) + for k in fields: + embed.add_field(name=str(k), value=str(fields[k]), inline=True) + await recipient.send(embed=embed) async def getemail(self, after): email = None @@ -98,6 +166,31 @@ class app(commands.Cog): message = "Timed Out. Message Server Admin with your email so They Can Add You Manually." await self.embederror(after, message) return None + + async def getusername(self, after): + username = None + await self.embedinfo(after, f"Welcome To Jellyfin! Just reply with a username for Jellyfin so we can add you!") + await self.embedinfo(after, f"I will wait 24 hours for your message, if you do not send it by then I will cancel the command.") + while (username is None): + def check(m): + return m.author == after and not m.guild + try: + username = await self.bot.wait_for('message', timeout=86400, check=check) + if(jelly.verify_username(JELLYFIN_SERVER_URL, JELLYFIN_API_KEY, str(username.content))): + return str(username.content) + else: + username = None + message = "This username is already choosen. Please select another Username." + await self.embederror(after, message) + continue + except asyncio.TimeoutError: + message = "Timed Out. Message Server Admin with your preferred username so They Can Add You Manually." + await self.embederror(after, message) + return None + except Exception as e: + await self.embederror(after, "Something went wrong. Please try again with another username.") + print (e) + username = None async def addtoplex(self, email, channel): @@ -123,6 +216,30 @@ class app(commands.Cog): else: await self.embederror(channel, 'Invalid email.') return False + + async def addtojellyfin(self, username, password, channel): + if not jelly.verify_username(JELLYFIN_SERVER_URL, JELLYFIN_API_KEY, username): + await self.embederror(channel, f'An account with username {username} already exists.') + return + + if jelly.add_user(JELLYFIN_SERVER_URL, JELLYFIN_API_KEY, username, password, jellyfin_libs): + await self.embedinfo(channel, 'User successfully added to Jellyfin') + return True + else: + await self.embederror(channel, 'There was an error adding this user to Jellyfin. Check logs for more info.') + return False + + async def removefromjellyfin(self, username, channel): + if jelly.verify_username(JELLYFIN_SERVER_URL, JELLYFIN_API_KEY, username): + await self.embederror(channel, f'Could not find account with username {username}.') + return + + if jelly.remove_user(JELLYFIN_SERVER_URL, JELLYFIN_API_KEY, username): + await self.embedinfo(channel, f'Successfully removed user {username} from Jellyfin.') + return True + else: + await self.embederror(channel, f'There was an error removing this user from Jellyfin. Check logs for more info.') + return False @commands.Cog.listener() async def on_member_update(self, before, after): @@ -130,38 +247,95 @@ class app(commands.Cog): return roles_in_guild = after.guild.roles role = None - for role_for_app in plex_roles: - for role_in_guild in roles_in_guild: - if role_in_guild.name == role_for_app: - role = role_in_guild - if role is not None and (role in after.roles and role not in before.roles): - email = await self.getemail(after) - if email is not None: - await self.embedinfo(after, "Got it we will be adding your email to plex shortly!") - if plexhelper.plexadd(plex,email,Plex_LIBS): - db.save_user(str(after.id), email) - await asyncio.sleep(5) - await self.embedinfo(after, 'You have Been Added To Plex! Login to plex and accept the invite!') - else: - await self.embedinfo(after, 'There was an error adding this email address. Message Server Admin.') - return + plex_processed = False + jellyfin_processed = False - elif role is not None and (role not in after.roles and role in before.roles): - try: - user_id = after.id - email = db.get_useremail(user_id) - plexhelper.plexremove(plex,email) - deleted = db.delete_user(user_id) - if deleted: - print("Removed {} from db".format(email)) - #await secure.send(plexname + ' ' + after.mention + ' was removed from plex') - else: - print("Cannot remove this user from db.") - except Exception as e: - print(e) - print("{} Cannot remove this user from plex.".format(email)) - return + # Check Plex roles + if plex_configured and USE_PLEX: + for role_for_app in plex_roles: + for role_in_guild in roles_in_guild: + if role_in_guild.name == role_for_app: + role = role_in_guild + + # Plex role was added + if role is not None and (role in after.roles and role not in before.roles): + email = await self.getemail(after) + if email is not None: + await self.embedinfo(after, "Got it we will be adding your email to plex shortly!") + if plexhelper.plexadd(plex,email,Plex_LIBS): + db.save_user_email(str(after.id), email) + await asyncio.sleep(5) + await self.embedinfo(after, 'You have Been Added To Plex! Login to plex and accept the invite!') + else: + await self.embedinfo(after, 'There was an error adding this email address. Message Server Admin.') + plex_processed = True + break + + # Plex role was removed + elif role is not None and (role not in after.roles and role in before.roles): + try: + user_id = after.id + email = db.get_useremail(user_id) + plexhelper.plexremove(plex,email) + deleted = db.remove_email(user_id) + if deleted: + print("Removed Plex email {} from db".format(after.name)) + #await secure.send(plexname + ' ' + after.mention + ' was removed from plex') + else: + print("Cannot remove Plex from this user.") + await self.embedinfo(after, "You have been removed from Plex") + except Exception as e: + print(e) + print("{} Cannot remove this user from plex.".format(email)) + plex_processed = True + break + if plex_processed: + break + + # Check Jellyfin roles + if jellyfin_configured and USE_JELLYFIN: + for role_for_app in jellyfin_roles: + for role_in_guild in roles_in_guild: + if role_in_guild.name == role_for_app: + role = role_in_guild + + # Jellyfin role was added + if role is not None and (role in after.roles and role not in before.roles): + username = await self.getusername(after) + if username is not None: + await self.embedinfo(after, "Got it we will be creating your Jellyfin account shortly!") + password = jelly.generate_password(16) + if jelly.add_user(JELLYFIN_SERVER_URL, JELLYFIN_API_KEY, username, password, jellyfin_libs): + db.save_user_jellyfin(str(after.id), username) + await asyncio.sleep(5) + await self.embedcustom(after, "You have been added to Jellyfin!", {'Username': username, 'Password': f"||{password}||"}) + await self.embedinfo(after, f"Go to {JELLYFIN_SERVER_URL} to log in!") + else: + await self.embedinfo(after, 'There was an error adding this user to Jellyfin. Message Server Admin.') + jellyfin_processed = True + break + + # Jellyfin role was removed + elif role is not None and (role not in after.roles and role in before.roles): + try: + user_id = after.id + username = db.get_jellyfin_username(user_id) + jelly.remove_user(JELLYFIN_SERVER_URL, JELLYFIN_API_KEY, username) + deleted = db.remove_jellyfin(user_id) + if deleted: + print("Removed Jellyfin from {}".format(after.name)) + #await secure.send(plexname + ' ' + after.mention + ' was removed from plex') + else: + print("Cannot remove Jellyfin from this user") + await self.embedinfo(after, "You have been removed from Jellyfin") + except Exception as e: + print(e) + print("{} Cannot remove this user from Jellyfin.".format(username)) + jellyfin_processed = True + break + if jellyfin_processed: + break @commands.Cog.listener() async def on_member_remove(self, member): @@ -181,6 +355,18 @@ class app(commands.Cog): async def plexremove(self, ctx, email): await self.removefromplex(email, ctx.channel) + @commands.has_permissions(administrator=True) + @commands.command(aliases=['jellyadd']) + async def jellyfininvite(self, ctx, username): + password = jelly.generate_password(16) + if await self.addtojellyfin(username, password, ctx.channel): + await self.embedcustom(ctx.author, "Jellyfin user created!", {'Username': username, 'Password': f"||{password}||"}) + + @commands.has_permissions(administrator=True) + @commands.command(aliases=['jellyrm']) + async def jellyfinremove(self, ctx, username): + await self.removefromjellyfin(username, ctx.channel) + @commands.has_permissions(administrator=True) @commands.command() async def dbadd(self, ctx, member: discord.Member, email, jellyfin_username): @@ -240,32 +426,34 @@ class app(commands.Cog): @commands.command() async def dbrm(self, ctx, position): embed = discord.Embed(title='Invitarr Database.') - all = db.read_useremail() # TODO: no need to read from DB or make a table here. + all = db.read_useremail() table = texttable.Texttable() - table.set_cols_dtype(["t", "t", "t"]) - table.set_cols_align(["c", "c", "c"]) - header = ("#", "Name", "Email") + table.set_cols_dtype(["t", "t", "t", "t"]) + table.set_cols_align(["c", "c", "c", "c"]) + header = ("#", "Name", "Email", "Jellyfin") table.add_row(header) for index, peoples in enumerate(all): index = index + 1 id = int(peoples[1]) dbuser = self.bot.get_user(id) - dbemail = peoples[2] + dbemail = peoples[2] if peoples[2] else "No Plex" + dbjellyfin = peoples[3] if peoples[3] else "No Jellyfin" try: username = dbuser.name except: username = "User Not Found." - embed.add_field(name=f"**{index}. {username}**", value=dbemail+'\n', inline=False) - table.add_row((index, username, dbemail)) + embed.add_field(name=f"**{index}. {username}**", value=dbemail+'\n'+dbjellyfin+'\n', inline=False) + table.add_row((index, username, dbemail, dbjellyfin)) try: position = int(position) - 1 id = all[position][1] - email = db.get_useremail(id) + discord_user = await self.bot.fetch_user(id) + username = discord_user.name deleted = db.delete_user(id) if deleted: - print("Removed {} from db".format(email)) - await self.embedinfo(ctx.channel,"Removed {} from db".format(email)) + print("Removed {} from db".format(username)) + await self.embedinfo(ctx.channel,"Removed {} from db".format(username)) else: await self.embederror(ctx.channel,"Cannot remove this user from db.") except Exception as e: diff --git a/app/bot/helper/confighelper.py b/app/bot/helper/confighelper.py index d10ace8..e28efff 100644 --- a/app/bot/helper/confighelper.py +++ b/app/bot/helper/confighelper.py @@ -8,7 +8,8 @@ config = configparser.ConfigParser() CONFIG_KEYS = ['username', 'password', 'discord_bot_token', 'plex_user', 'plex_pass', 'plex_roles', 'plex_server_name', 'plex_libs', 'owner_id', 'channel_id', - 'auto_remove_user'] + 'auto_remove_user', 'jellyfin_api_key', 'jellyfin_server_url', 'jellyfin_roles', + 'jellyfin_libs', 'plex_enabled', 'jellyfin_enabled'] # settings Discord_bot_token = "" @@ -17,6 +18,11 @@ PLEXUSER = "" PLEXPASS = "" PLEX_SERVER_NAME = "" Plex_LIBS = None +JELLYFIN_SERVER_URL = "" +JELLYFIN_API_KEY = "" +jellyfin_libs = "" +jellyfin_roles = None + switch = 0 @@ -36,26 +42,63 @@ try: except Exception as e: pass -if(path.exists('app/config/config.ini')): +if(path.exists(CONFIG_PATH)): + config = configparser.ConfigParser() + config.read(CONFIG_PATH) + + # Get Plex config try: - config = configparser.ConfigParser() - config.read(CONFIG_PATH) PLEXUSER = config.get(BOT_SECTION, 'plex_user') PLEXPASS = config.get(BOT_SECTION, 'plex_pass') PLEX_SERVER_NAME = config.get(BOT_SECTION, 'plex_server_name') except: - pass + print("Could not load plex config") -if(path.exists('app/config/config.ini')): + # Get Plex roles config try: - roles = config.get(BOT_SECTION, 'plex_roles') + plex_roles = config.get(BOT_SECTION, 'plex_roles') except: - pass -if(path.exists('app/config/config.ini')): + print("Could not get Plex roles config") + + # Get Plex libs config try: Plex_LIBS = config.get(BOT_SECTION, 'plex_libs') except: - pass + print("Could not get Plex libs config") + + + # Get Jellyfin config + try: + JELLYFIN_SERVER_URL = config.get(BOT_SECTION, 'jellyfin_server_url') + JELLYFIN_API_KEY = config.get(BOT_SECTION, "jellyfin_api_key") + except: + print("Could not load Jellyfin config") + + # Get Jellyfin roles config + try: + jellyfin_roles = config.get(BOT_SECTION, 'jellyfin_roles') + except: + print("Could not get Jellyfin roles config") + + # Get Jellyfin libs config + try: + jellyfin_libs = config.get(BOT_SECTION, 'jellyfin_libs') + except: + print("Could not get Jellyfin libs config") + + # Get Enable config + try: + USE_JELLYFIN = config.get(BOT_SECTION, 'jellyfin_enabled') + except: + print("Could not get Jellyfin enable config. Defaulting to False") + USE_Jellyfin = False + + try: + USE_PLEX = config.get(BOT_SECTION, "plex_enabled") + except: + print("Could not get Plex enable config. Defaulting to False") + USE_PLEX = False + def get_config(): """ Function to return current config diff --git a/app/bot/helper/db.py b/app/bot/helper/db.py index 6312820..31be475 100644 --- a/app/bot/helper/db.py +++ b/app/bot/helper/db.py @@ -94,7 +94,7 @@ def get_useremail(username): if email: return email else: - return "No users found" + return "No email found" except: return "error in fetching from db" else: @@ -122,6 +122,32 @@ def get_jellyfin_username(username): else: return "username cannot be empty" +def remove_email(username): + """ + Sets email of discord user to null in database + """ + if username: + conn.execute(f"UPDATE clients SET email = null WHERE discord_username = '{username}'") + conn.commit() + print(f"Email removed from user {username} in database") + return True + else: + print(f"Username cannot be empty.") + return False + +def remove_jellyfin(username): + """ + Sets jellyfin username of discord user to null in database + """ + if username: + conn.execute(f"UPDATE clients SET jellyfin_username = null WHERE discord_username = '{username}'") + conn.commit() + print(f"Jellyfin username removed from user {username} in database") + return True + else: + print(f"Username cannot be empty.") + return False + def delete_user(username): if username: diff --git a/app/bot/helper/jellyfinhelper.py b/app/bot/helper/jellyfinhelper.py new file mode 100644 index 0000000..f598d38 --- /dev/null +++ b/app/bot/helper/jellyfinhelper.py @@ -0,0 +1,175 @@ +import requests +import random +import string + +def add_user(jellyfin_url, jellyfin_api_key, username, password, jellyfin_libs): + try: + url = f"{jellyfin_url}/Users/New" + + querystring = {"api_key":jellyfin_api_key} + payload = { + "Name": username, + "Password": password + } + headers = {"Content-Type": "application/json"} + response = requests.request("POST", url, json=payload, headers=headers, params=querystring) + userId = response.json()["Id"] + + if response.status_code != 200: + print(f"Error creating new Jellyfin user: {response.text}") + return False + + # Grant access to User + url = f"{jellyfin_url}/Users/{userId}/Policy" + + querystring = {"api_key":jellyfin_api_key} + + enabled_folders = [] + server_libs = get_libraries(jellyfin_url, jellyfin_api_key) + + if jellyfin_libs[0] != "all": + for lib in jellyfin_libs: + found = False + for server_lib in server_libs: + if lib == server_lib['Name']: + enabled_folders.append(server_lib['ItemId']) + found = True + if not found: + print(f"Couldn't find Jellyfin Library: {lib}") + + payload = { + "IsAdministrator": False, + "IsHidden": True, + "IsDisabled": False, + "BlockedTags": [], + "EnableUserPreferenceAccess": True, + "AccessSchedules": [], + "BlockUnratedItems": [], + "EnableRemoteControlOfOtherUsers": False, + "EnableSharedDeviceControl": True, + "EnableRemoteAccess": True, + "EnableLiveTvManagement": True, + "EnableLiveTvAccess": True, + "EnableMediaPlayback": True, + "EnableAudioPlaybackTranscoding": True, + "EnableVideoPlaybackTranscoding": True, + "EnablePlaybackRemuxing": True, + "ForceRemoteSourceTranscoding": False, + "EnableContentDeletion": False, + "EnableContentDeletionFromFolders": [], + "EnableContentDownloading": True, + "EnableSyncTranscoding": True, + "EnableMediaConversion": True, + "EnabledDevices": [], + "EnableAllDevices": True, + "EnabledChannels": [], + "EnableAllChannels": False, + "EnabledFolders": enabled_folders, + "EnableAllFolders": jellyfin_libs[0] == "all", + "InvalidLoginAttemptCount": 0, + "LoginAttemptsBeforeLockout": -1, + "MaxActiveSessions": 0, + "EnablePublicSharing": True, + "BlockedMediaFolders": [], + "BlockedChannels": [], + "RemoteClientBitrateLimit": 0, + "AuthenticationProviderId": "Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider", + "PasswordResetProviderId": "Jellyfin.Server.Implementations.Users.DefaultPasswordResetProvider", + "SyncPlayAccess": "CreateAndJoinGroups" + } + headers = {"content-type": "application/json"} + + response = requests.request("POST", url, json=payload, headers=headers, params=querystring) + + if response.status_code == 200 or response.status_code == 204: + return True + else: + print(f"Error setting user permissions: {response.text}") + + except Exception as e: + print(e) + return False + +def get_libraries(jellyfin_url, jellyfin_api_key): + url = f"{jellyfin_url}/Library/VirtualFolders" + querystring = {"api_key":jellyfin_api_key} + response = requests.request("GET", url, params=querystring) + + return response.json() + + +def verify_username(jellyfin_url, jellyfin_api_key, username): + users = get_users(jellyfin_url, jellyfin_api_key) + valid = True + for user in users: + if user['Name'] == username: + valid = False + break + + return valid + +def remove_user(jellyfin_url, jellyfin_api_key, jellyfin_username): + try: + # Get User ID + users = get_users(jellyfin_url, jellyfin_api_key) + userId = None + for user in users: + if user['Name'].lower() == jellyfin_username.lower(): + userId = user['Id'] + + if userId is None: + # User not found + print(f"Error removing user {jellyfin_username} from Jellyfin: Could not find user.") + return False + + # Delete User + url = f"{jellyfin_url}/Users/{userId}" + + querystring = {"api_key":jellyfin_api_key} + response = requests.request("DELETE", url, params=querystring) + + if response.status_code == 204 or response.status_code == 200: + return True + else: + print(f"Error deleting Jellyfin user: {response.text}") + except Exception as e: + print(e) + return False + +def get_users(jellyfin_url, jellyfin_api_key): + url = f"{jellyfin_url}/Users" + + querystring = {"api_key":jellyfin_api_key} + response = requests.request("GET", url, params=querystring) + + return response.json() + +def generate_password(length, lower=True, upper=True, numbers=True, symbols=True): + character_list = [] + if not (lower or upper or numbers or symbols): + raise ValueError("At least one character type must be provided") + + if lower: + character_list += string.ascii_lowercase + if upper: + character_list += string.ascii_uppercase + if numbers: + character_list += string.digits + if symbols: + character_list += string.punctuation + + return "".join(random.choice(character_list) for i in range(length)) + +def get_config(jellyfin_url, jellyfin_api_key): + url = f"{jellyfin_url}/System/Configuration" + + querystring = {"api_key":jellyfin_api_key} + response = requests.request("GET", url, params=querystring) + return response.json() + +def get_status(jellyfin_url, jellyfin_api_key): + url = f"{jellyfin_url}/System/Configuration" + + querystring = {"api_key":jellyfin_api_key} + response = requests.request("GET", url, params=querystring) + return response.status_code \ No newline at end of file diff --git a/app/bot/helper/plexhelper.py b/app/bot/helper/plexhelper.py index 412e181..2ac286e 100644 --- a/app/bot/helper/plexhelper.py +++ b/app/bot/helper/plexhelper.py @@ -46,7 +46,4 @@ def plexremoveinvite(plex, plexname): def verifyemail(addressToVerify): regex = '^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$' match = re.match(regex, addressToVerify.lower()) - if match == None: - return False - else: - return True \ No newline at end of file + return match != None \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d438d48..fdf4964 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,5 @@ plex.py==0.9.0 PlexAPI==4.0.0 texttable python-dotenv +jellyfin-apiclient-python +requests \ No newline at end of file diff --git a/run.py b/run.py index 27a4513..f276724 100644 --- a/run.py +++ b/run.py @@ -4,8 +4,9 @@ from discord.ext import commands, tasks from discord.utils import get import asyncio import sys -from app.bot.helper.confighelper import switch, Discord_bot_token, plex_roles +from app.bot.helper.confighelper import switch, Discord_bot_token, plex_roles, jellyfin_roles import app.bot.helper.confighelper as confighelper +import app.bot.helper.jellyfinhelper as jelly maxroles = 10 print(f"Discord Bot Token: {Discord_bot_token}") @@ -15,11 +16,16 @@ if plex_roles is None: else: plex_roles = list(plex_roles.split(',')) +if jellyfin_roles is None: + jellyfin_roles = [] +else: + jellyfin_roles = list(jellyfin_roles.split(',')) + if switch == 0: print("Missing Config.") sys.exit() -print("V 1.0") +print("V 1.1") intents = discord.Intents.default() intents.members = True @@ -39,12 +45,21 @@ async def on_message(message): return await bot.process_commands(message) +# these were copied from the app object. They could be made static instead but I'm lazy. +async def embederror(author, message): + embed1 = discord.Embed(title="ERROR",description=message, color=0xf50000) + await author.send(embed=embed1) + +async def embedinfo(author, message): + embed1 = discord.Embed(title=message, color=0x00F500) + await author.send(embed=embed1) + def reload(): bot.reload_extension(f'app.bot.cogs.app') -async def getplex(ctx, type): +async def getuser(ctx, server, type): value = None - await ctx.author.send("Please reply with your Plex {}:".format(type)) + await ctx.author.send("Please reply with your {} {}:".format(server, type)) while(value == None): def check(m): return m.author == ctx.author and not m.guild @@ -71,18 +86,18 @@ async def plexroleadd(ctx, role: discord.Role): @bot.command() @commands.has_permissions(administrator=True) async def setupplex(ctx): - username = "" - pasword = "" - servername = "" - username = await getplex(ctx, "username") + username = None + password = None + servername = None + username = await getuser(ctx, "Plex", "username") if username is None: return else: - password = await getplex(ctx, "password") + password = await getuser(ctx, "Plex", "password") if password is None: return else: - servername = await getplex(ctx, "servername") + servername = await getuser(ctx, "Plex", "servername") if servername is None: return else: @@ -95,19 +110,131 @@ async def setupplex(ctx): await ctx.author.send("Bot has been restarted. Give it a few seconds. Please check logs and make sure you see the line: `Logged into plex`. If not run this command again and make sure you enter the right values. ") print("Bot has been restarted. Give it a few seconds.") +@bot.command() +@commands.has_permissions(administrator=True) +async def jellyroleadd(ctx, role: discord.Role): + if len(jellyfin_roles) <= maxroles: + jellyfin_roles.append(role.name) + print (f"new jellyfin roles: {jellyfin_roles}") + saveroles = ",".join(jellyfin_roles) + print (f"saveroles: {saveroles}") + confighelper.change_config("jellyfin_roles", saveroles) + await ctx.author.send("Updated Jellyfin roles. Bot is restarting. Please wait.") + print("Jellyfin roles updated. Restarting bot.") + reload() + await ctx.author.send("Bot has been restarted. Give it a few seconds.") + print("Bot has been restarted. Give it a few seconds.") + +@bot.command() +@commands.has_permissions(administrator=True) +async def setupjelly(ctx): + jellyfin_api_key = None + jellyfin_server_url = None + + jellyfin_server_url = await getuser(ctx, "Jellyfin", "Server Url") + if jellyfin_server_url is None: + return + + jellyfin_api_key = await getuser(ctx, "Jellyfin", "API Key") + if jellyfin_api_key is None: + return + + try: + server_status = jelly.get_status(jellyfin_server_url, jellyfin_api_key) + if server_status == 200: + pass + elif server_status == 401: + # Unauthorized + await embederror(ctx.author, "API key provided is invalid") + return + elif server_status == 403: + # Forbidden + await embederror(ctx.author, "API key provided does not have permissions") + return + elif server_status == 404: + # page not found + await embederror(ctx.author, "Server endpoint provided was not found") + return + except Exception as e: + print("Exception while testing Jellyfin connection") + print(e) + await embederror(ctx.author, "Could not connect to server. Check logs for more details.") + return + + + jellyfin_server_url = jellyfin_server_url.rstrip('/') + confighelper.change_config("jellyfin_server_url", str(jellyfin_server_url)) + confighelper.change_config("jellyfin_api_key", str(jellyfin_api_key)) + print("Jellyfin server URL and API key updated. Restarting bot.") + await ctx.author.send("Jellyfin server URL and API key updated. Restarting bot.") + reload() + await ctx.author.send("Bot has been restarted. Give it a few seconds. Please check logs and make sure you see the line: `Connected to Jellyfin`. If not run this command again and make sure you enter the right values. ") + print("Bot has been restarted. Give it a few seconds.") + + @bot.command() @commands.has_permissions(administrator=True) async def setupplexlibs(ctx): - libs = "" - libs = await getplex(ctx, "libs") + libs = await getuser(ctx, "Plex", "libs") if libs is None: return else: confighelper.change_config("plex_libs", str(libs)) print("Plex libraries updated. Restarting bot. Please wait.") reload() - await ctx.author.send("Bot has been restarted. Give it a few seconds. Please check logs and make sure you see the line: `Logged into plex`. If not run this command again and make sure you enter the right values. ") + await ctx.author.send("Bot has been restarted. Give it a few seconds.") print("Bot has been restarted. Give it a few seconds.") +@bot.command() +@commands.has_permissions(administrator=True) +async def setupjellylibs(ctx): + libs = await getuser(ctx, "Jellyfin", "libs") + if libs is None: + return + else: + confighelper.change_config("jellyfin_libs", str(libs)) + print("Jellyfin libraries updated. Restarting bot. Please wait.") + reload() + await ctx.author.send("Bot has been restarted. Give it a few seconds.") + print("Bot has been restarted. Give it a few seconds.") + +# Enable / Disable Plex integration +@bot.command(aliases=["plexenable"]) +@commands.has_permissions(administrator=True) +async def enableplex(ctx): + confighelper.change_config("plex_enabled", True) + print("Plex enabled, reloading server") + reload() + await ctx.author.send("Bot has restarted. Give it a few seconds.") + print("Bot has restarted. Give it a few seconds.") + +@bot.command(aliases=["plexdisable"]) +@commands.has_permissions(administrator=True) +async def disableplex(ctx): + confighelper.change_config("plex_enabled", False) + print("Plex disabled, reloading server") + reload() + await ctx.author.send("Bot has restarted. Give it a few seconds.") + print("Bot has restarted. Give it a few seconds.") + +# Enable / Disable Jellyfin integration +@bot.command(aliases=["jellyenable"]) +@commands.has_permissions(administrator=True) +async def enablejellyfin(ctx): + confighelper.change_config("jellyfin_enabled", True) + print("Jellyfin enabled, reloading server") + reload() + await ctx.author.send("Bot has restarted. Give it a few seconds.") + print("Bot has restarted. Give it a few seconds.") + +@bot.command(aliases=["jellydisable"]) +@commands.has_permissions(administrator=True) +async def disablejellyfin(ctx): + confighelper.change_config("jellyfin_enabled", False) + print("Jellyfin disabled, reloading server") + reload() + await ctx.author.send("Bot has restarted. Give it a few seconds.") + print("Bot has restarted. Give it a few seconds.") + bot.load_extension(f'app.bot.cogs.app') bot.run(Discord_bot_token) \ No newline at end of file