From 755c9d3da6cbb01d46132cc7ed3ef555486f7f04 Mon Sep 17 00:00:00 2001 From: TPD94 <> Date: Fri, 17 Nov 2023 01:33:30 -0500 Subject: [PATCH] CDRM-API-Bot --- CDM-API.service | 13 ++ bot-token.txt | 1 + main.py | 504 ++++++++++++++++++++++++++++++++++++++++++++++++ serve.yml | 8 + 4 files changed, 526 insertions(+) create mode 100644 CDM-API.service create mode 100644 bot-token.txt create mode 100644 main.py create mode 100644 serve.yml diff --git a/CDM-API.service b/CDM-API.service new file mode 100644 index 0000000..a584368 --- /dev/null +++ b/CDM-API.service @@ -0,0 +1,13 @@ +[Unit] +Description=CDM-API +After=syslog.target network.target + +[Service] +User=root +Type=simple +WorkingDirectory=/opt/CDRM-API-Bot +ExecStart=/usr/local/bin/pywidevine serve /opt/CDRM-API-Bot/serve.yml +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/bot-token.txt b/bot-token.txt new file mode 100644 index 0000000..72ba3eb --- /dev/null +++ b/bot-token.txt @@ -0,0 +1 @@ +Delete this and place your bot token on this line \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..518d977 --- /dev/null +++ b/main.py @@ -0,0 +1,504 @@ +import os +import glob +import sqlite3 +import interactions +import yaml +import random +import string +import subprocess +from interactions import * + +# Get current working directory +main_directory = os.getcwd() + +# Create database and table for user and api keys +if not os.path.isfile(f"{main_directory}/database.db"): + dbconnection = sqlite3.connect("database.db") + dbcursor = dbconnection.cursor() + dbcursor.execute( + 'CREATE TABLE IF NOT EXISTS "DATABASE" ( "user" TEXT, "apikey" TEXT, "status" TEXT, PRIMARY KEY("user") )') + dbconnection.close() + +# Making sure a serve.yml file exists and using that as the config file. +try: + serve = glob.glob(f'{main_directory}/serve.yml')[0] +except: + with open(f'{main_directory}/Instructions.txt', 'w') as Instructions: + print(f"Please create your serve.yml in {main_directory}") + Instructions.write("Place your serve.yml in this directory!") + exit() + +# Check if discord bot token file exists, if not create file +if not os.path.isfile(f"{main_directory}/bot-token.txt"): + with open(f'{main_directory}/bot-token.txt', 'w') as discord_bot_token: + print("Please put your bot token in bot-token.txt") + discord_bot_token.write("Delete this and place your bot token on this line") + exit() + +# Check if bot token exists and if it has been changed +with open(f'{main_directory}/bot-token.txt') as discord_bot_token: + bot_token = discord_bot_token.readline() + if bot_token == "Delete this and place your bot token on this line": + print("Please put your bot token in bot-token.txt") + exit() + +bot = interactions.Client(token=bot_token) + + +# API Key generator function +async def generate_api_key(): + key = "".join(random.choices(string.ascii_letters + string.digits, k=32)) + return key + + +# Initial API Request function +async def initial_request(user: str): + dbconnection = sqlite3.connect("database.db") + dbcursor = dbconnection.cursor() + dbcursor.execute("INSERT or REPLACE INTO database VALUES (?, ?, ?)", (user, "", "requested")) + dbconnection.commit() + dbconnection.close() + + +# Approve request function +async def approve_request(user: str): + # Open DB and approve the request, store the API key. + dbconnection = sqlite3.connect("database.db") + dbcursor = dbconnection.cursor() + api_key = await generate_api_key() + dbcursor.execute("INSERT or REPLACE INTO database VALUES (?, ?, ?)", (user, api_key, "approved")) + dbconnection.commit() + dbconnection.close() + + # Open YAML file + with open(serve) as serve_yaml: + yaml_dict = yaml.safe_load(serve_yaml) + + # Add new entries + yaml_dict["users"][api_key] = {"username": user} + yaml_dict["users"][api_key]["devices"] = ["CDM"] + + # Write the new YAML + with open(serve, "w") as new_serve_yaml: + yaml.safe_dump(yaml_dict, new_serve_yaml) + + return api_key + + +# Deny user request function +async def deny_request(user: str): + # Open DB and deny the request. + dbconnection = sqlite3.connect("database.db") + dbcursor = dbconnection.cursor() + dbcursor.execute("INSERT or REPLACE INTO database VALUES (?, ?, ?)", (user, "", "denied")) + dbconnection.commit() + dbconnection.close() + + +# Define check request status function +async def check_request_status(user: str): + dbconnection = sqlite3.connect("database.db") + dbcursor = dbconnection.cursor() + dbcursor.execute("SELECT status FROM database WHERE user = :user", {"user": user}) + user_status = dbcursor.fetchall() + formatted_user_status = '' + for character in str(user_status): + if character.isalnum(): + formatted_user_status += character + dbconnection.close() + return formatted_user_status + + +# Define retrieve API key function +async def retrieve_api_key(user: str): + dbconnection = sqlite3.connect("database.db") + dbcursor = dbconnection.cursor() + dbcursor.execute("SELECT apikey FROM database WHERE user = :user", {"user": user}) + user_api_key = dbcursor.fetchall() + formatted_api_key = '' + for character in str(user_api_key): + if character.isalnum(): + formatted_api_key += character + dbconnection.close() + return formatted_api_key + + +# Revoke user API key function +async def revoke_key(user: str): + # Find the users API key to remove from the serve.yml + user_api_key = await retrieve_api_key(user) + + # Open DB and revoke the api key. + dbconnection = sqlite3.connect("database.db") + dbcursor = dbconnection.cursor() + dbcursor.execute("INSERT or REPLACE INTO database VALUES (?, ?, ?)", (user, "", "revoked")) + dbconnection.commit() + dbconnection.close() + + # Open the YAML file + with open(serve) as serve_yaml: + yaml_dict = yaml.safe_load(serve_yaml) + + # Delete the API key entry + del yaml_dict["users"][user_api_key] + + # Save the YAML file + with open(serve, "w") as new_serve_yaml: + yaml.safe_dump(yaml_dict, new_serve_yaml) + + +# Reset user status +async def reset_status(user: str): + # Open DB and reset the user status. + dbconnection = sqlite3.connect("database.db") + dbcursor = dbconnection.cursor() + dbcursor.execute("INSERT or REPLACE INTO database VALUES (?, ?, ?)", (user, "", "")) + dbconnection.commit() + dbconnection.close() + + +# Request API key slash command +@slash_command(name="request_api_key", description="Request an API key from https://api.cdm-project.com/") +async def request_api_key(ctx: SlashContext): + # Set admin channel + admin_channel = await bot.fetch_channel(1174897288745861120, force=True) + + # Get user information and set DM + user_id = ctx.user.id + user_name = ctx.user.username + user_dm = bot.get_user(user_id) + + # API request modal + api_request_modal = Modal( + ParagraphText(label="Why do you want an API key? (Be specific)", custom_id="request_reason"), + title="CDM API access", + ) + + # API request buttons + api_request_buttons: list[ActionRow] = [ + ActionRow( + Button( + custom_id=f"{str(user_id)}Approve", + style=ButtonStyle.GREEN, + label="Approve", + ), + Button( + custom_id=f"{str(user_id)}Deny", + style=ButtonStyle.RED, + label="Deny", + ) + ) + ] + + # API approved button + approved_button = Button( + custom_id=str(user_id), + style=ButtonStyle.BLUE, + label="Approved!", + disabled=True + ) + + # API denied button + denied_button = Button( + custom_id=str(user_id), + style=ButtonStyle.DANGER, + label="Denied!", + disabled=True + ) + + # Check if user has submitted an application before + + # Send application and request if not already submitted + if await check_request_status(str(user_id)) != "requested" and await check_request_status( + str(user_id)) != "approved" and await check_request_status(str(user_id)) != "revoked": + await ctx.send_modal(modal=api_request_modal) + modal_ctx: ModalContext = await ctx.bot.wait_for_modal(api_request_modal) + await modal_ctx.send(f"{user_name}, Your application for an API key has been submitted.") + await initial_request(str(user_id)) + admin_request_message = await admin_channel.send( + f"{user_name} has requested API access.\n```{modal_ctx.responses['request_reason']}```", + components=api_request_buttons) + status = await bot.wait_for_component(components=api_request_buttons) + + # Check if the button was clicked for approve and send response + if status.ctx.custom_id == f"{str(user_id)}Approve": + await status.ctx.edit_origin(components=approved_button) + await admin_request_message.edit(content=f"Approved {user_name}!") + api_key = await approve_request(str(user_id)) + api_restart_command = "sudo systemctl restart CDM-API.service" + subprocess.run(api_restart_command, shell=True) + await user_dm.send(f"Your request has been approved!\n\nYour API Key: `{api_key}`") + await ctx.member.add_role(1107788510129295510) + + # Check if the button was clicked for deny and send response + elif status.ctx.custom_id == f"{str(user_id)}Deny": + await status.ctx.edit_origin(components=denied_button) + await admin_request_message.edit(content=f"Denied {user_name}!") + await deny_request(str(user_id)) + await user_dm.send(f"You've been denied! Please either submit with a better reason or donate!") + + # Send message if already submitted + else: + await ctx.send("Application already submitted, please use `/check_status` to view your application status") + + +# Check status slash command +@slash_command(name="check_status", description="Check application status / API key") +async def check_status(ctx: SlashContext): + # API key approved embed + api_key_approved_embed = interactions.Embed( + title=f"API Key request", + description=f"Approved! ✔") + api_key_approved_embed.set_footer(text=f"https://api.cdm-project.com/") + + # API Key denied embed + api_key_denied_embed = interactions.Embed( + title=f"API Key request", + description=f"Denied ✖") + api_key_denied_embed.set_footer(text=f"https://api.cdm-project.com/") + + # API Key denied embed + api_key_revoked_embed = interactions.Embed( + title=f"API Key request", + description=f"Revoked! ✖") + api_key_denied_embed.set_footer(text=f"https://api.cdm-project.com/") + + # API key pending embed + api_key_pending_embed = interactions.Embed( + title=f"API Key request", + description=f"Pending ❓") + api_key_pending_embed.set_footer(text=f"https://api.cdm-project.com/") + + # Get User ID from interaction + user_id = ctx.user.id + + # Check status of API key request + status = await check_request_status(str(user_id)) + + # Handle response based on status + if str(status) == "requested": + await ctx.send(f"Your API status is `{str(status).strip('[](),')}`", embeds=api_key_pending_embed) + elif str(status) == "denied": + await ctx.send(f"Your API status is `{str(status).strip('[](),')}`", embeds=api_key_denied_embed) + elif str(status) == "revoked": + await ctx.send(f"Your API status is `{str(status).strip('[](),')}`", embeds=api_key_revoked_embed) + elif str(status) == "approved": + user_api_key = await retrieve_api_key(str(user_id)) + api_key_approved_embed.add_field("API Key", user_api_key) + await ctx.send(f"Your API status is `{str(status).strip('[](),')}`", embeds=api_key_approved_embed, + ephemeral=True) + else: + await ctx.send("API permissions not requested yet, please use `/request_api`") + + +# Admin check status slash command +@slash_command( + name="check_user_status", + description="Check application status / API key for specific user", + default_member_permissions=Permissions.ADMINISTRATOR, + dm_permission=False +) +@slash_option( + name="discord_id", + description="Discord ID of the user you want to check.", + required=True, + opt_type=OptionType.STRING +) +async def check_user_status(ctx: SlashContext, discord_id: str): + # API key approved embed + api_key_approved_embed = interactions.Embed( + title=f"API Key request", + description=f"Approved! ✔") + api_key_approved_embed.set_footer(text=f"https://api.cdm-project.com/") + + # API Key denied embed + api_key_denied_embed = interactions.Embed( + title=f"API Key request", + description=f"Denied ✖") + api_key_denied_embed.set_footer(text=f"https://api.cdm-project.com/") + + # API Key denied embed + api_key_revoked_embed = interactions.Embed( + title=f"API Key request", + description=f"Revoked! ✖") + api_key_denied_embed.set_footer(text=f"https://api.cdm-project.com/") + + # API key pending embed + api_key_pending_embed = interactions.Embed( + title=f"API Key request", + description=f"Pending ❓") + api_key_pending_embed.set_footer(text=f"https://api.cdm-project.com/") + + # Check status of API key request + status = await check_request_status(discord_id) + + # Handle response based on status + if str(status) == "requested": + await ctx.send(f"{discord_id} API status is `{str(status).strip('[](),')}`", embeds=api_key_pending_embed, + ephemeral=True) + elif str(status) == "denied": + await ctx.send(f"{discord_id} API status is `{str(status).strip('[](),')}`", embeds=api_key_denied_embed, + ephemeral=True) + elif str(status) == "revoked": + await ctx.send(f"{discord_id} API status is `{str(status).strip('[](),')}`", embeds=api_key_revoked_embed, + ephemeral=False) + elif str(status) == "approved": + user_api_key = await retrieve_api_key(discord_id) + api_key_approved_embed.add_field("API Key", user_api_key) + await ctx.send(f"{discord_id} API status is `{str(status).strip('[](),')}`", embeds=api_key_approved_embed, + ephemeral=True) + else: + await ctx.send(f"{discord_id} permissions not requested yet") + + +# Revoke API Key slash command +@slash_command( + name="revoke_api_key", + default_member_permissions=Permissions.ADMINISTRATOR, + dm_permission=False, + description="Revoke the users API key" +) +@slash_option( + name="discord_id", + description="Discord ID of the user you want to revoke.", + required=True, + opt_type=OptionType.STRING +) +async def revoke_api_key(ctx: SlashContext, discord_id: str): + # Retrieve the users API key + api_key = await retrieve_api_key(discord_id) + + # API revoke buttons + api_revoke_buttons: list[ActionRow] = [ + ActionRow( + Button( + custom_id=f"{discord_id}Revoke", + style=ButtonStyle.DANGER, + label="Revoke?", + disabled=False + ), + Button( + custom_id=f"{discord_id}Cancel", + style=ButtonStyle.BLURPLE, + label="Cancel", + disabled=False + ) + ) + ] + + # API revoked button + revoked_button = Button( + custom_id=f"{discord_id}Revoked", + style=ButtonStyle.GREEN, + label="Revoked!", + disabled=True + ) + + # API revoked canceled button + canceled_button = Button( + custom_id=f"{discord_id}Canceled", + style=ButtonStyle.GRAY, + label="Canceled.", + disabled=True + ) + + # Revoke embed + revoke_api_key_embed = interactions.Embed( + title=f"Revoke API key?", + description=f"Revoke `{api_key}` ❓") + + # Revoked embed + revoked_api_key_embed = interactions.Embed( + title=f"Revoke API key?", + description=f"Revoked `{api_key}` ✔") + + # Send message to confirm revocation + revoke_message = await ctx.send(embeds=revoke_api_key_embed, components=api_revoke_buttons, ephemeral=False) + button_status = await bot.wait_for_component(components=api_revoke_buttons) + + # Check if the button was clicked for revoke and send response + if button_status.ctx.custom_id == f"{discord_id}Revoke": + await revoke_key(discord_id) + api_restart_command = "sudo systemctl restart CDM-API.service" + subprocess.run(api_restart_command, shell=True) + await button_status.ctx.edit_origin(components=revoked_button) + await revoke_message.edit(embeds=revoked_api_key_embed) + + # Check if the button was clicked for cancel and send response + elif button_status.ctx.custom_id == f"{discord_id}Cancel": + await button_status.ctx.edit_origin(components=canceled_button) + + +@slash_command( + name="reset_user_status", + default_member_permissions=Permissions.ADMINISTRATOR, + dm_permission=False, + description="Reset the users API status" +) +@slash_option( + name="discord_id", + description="Discord ID of the user you want to reset.", + required=True, + opt_type=OptionType.STRING +) +async def reset_user_status(ctx: SlashContext, discord_id: str): + # API reset buttons + api_reset_buttons: list[ActionRow] = [ + ActionRow( + Button( + custom_id=f"{discord_id}Reset", + style=ButtonStyle.DANGER, + label="Reset?", + disabled=False + ), + Button( + custom_id=f"{discord_id}Cancel", + style=ButtonStyle.BLURPLE, + label="Cancel", + disabled=False + ) + ) + ] + + # API reset confirm button + reset_button = Button( + custom_id=f"{discord_id}Restart", + style=ButtonStyle.GREEN, + label="Reset!", + disabled=True + ) + + # API reset canceled button + canceled_button = Button( + custom_id=f"{discord_id}Canceled", + style=ButtonStyle.GRAY, + label="Canceled.", + disabled=True + ) + + # Reset embed + reset_status_embed = interactions.Embed( + title=f"Reset User status?", + description=f"Reset `{discord_id}` ❓") + + # Reset confirm embed + reset_status_confirm = interactions.Embed( + title=f"Reset User status?", + description=f"Reset `{discord_id}` ✔") + + # Send message to confirm revocation + reset_message = await ctx.send(embeds=reset_status_embed, components=api_reset_buttons, ephemeral=False) + button_status = await bot.wait_for_component(components=api_reset_buttons) + + # Check if the button was clicked for reset and send response + if button_status.ctx.custom_id == f"{discord_id}Reset": + await reset_status(discord_id) + await button_status.ctx.edit_origin(components=reset_button) + await reset_message.edit(embeds=reset_status_confirm) + + # Check if the button was clicked for cancel and send response + elif button_status.ctx.custom_id == f"{discord_id}Cancel": + await button_status.ctx.edit_origin(components=canceled_button) + + +bot.start() diff --git a/serve.yml b/serve.yml new file mode 100644 index 0000000..d196a7a --- /dev/null +++ b/serve.yml @@ -0,0 +1,8 @@ +devices: +- ./CDM.wvd +force_privacy_mode: false +users: + DonatorsAre1337: + device: + - CDM + username: Donator