import discord from discord.ext import commands, tasks from discord import app_commands import os from dotenv import load_dotenv import sqlite3 import logging from datetime import datetime, timezone from discord import ui, Interaction import random import sys import requests import time ## Defines ## # Load environment variables from .env file load_dotenv() # Load bot token and welcome channel id from environment variables TOKEN = os.getenv('BOT_TOKEN') # Setup logging logging.basicConfig(filename='bot.log', level=logging.INFO) ## Function and Class Definitions ## class PersistentViewBot(commands.Bot): def __init__(self): # Define intents intents = discord.Intents.none() # intents.auto_moderation = True # intents.auto_moderation_configuration = True # intents.auto_moderation_execution = True # intents.bans = True # intents.dm_messages = True # intents.dm_reactions = True # intents.dm_typing = True # intents.emojis = True # intents.emojis_and_stickers = True # intents.guild_messages = True # intents.guild_reactions = True # intents.guild_scheduled_events = True # intents.guild_typing = True intents.guilds = True # intents.integrations = True # intents.invites = True # intents.members = True intents.message_content = True intents.messages = True # intents.moderation = True # intents.presences = True # intents.reactions = True # intents.typing = True # intents.value = True # intents.voice_states = True # intents.webhooks = True super().__init__(command_prefix=commands.when_mentioned_or("/"), intents=intents) # async def setup_hook(self) -> None: # self.add_view(OnboardButtons()) #self.add_view(OnboardButtons()) #Add More views with more add_view commands. ## Instantiate bot ## bot = PersistentViewBot() @bot.event async def on_ready(): print(f'Logged in as {bot.user}') # await welcome_message() with sqlite3.connect('bot_data.db') as conn: c = conn.cursor() print("Users in the DB:") c.execute("SELECT id, username FROM users") for row in c.fetchall(): print(f' ID: {row[0]} User: {row[1]}') print("Servers in the DB:") c.execute("SELECT id, join_url FROM mc_servers") for row in c.fetchall(): print(f' ID: {row[0]} URL: {row[1]}') print("Permissions in the DB:") c.execute("SELECT user_id, mc_server_id, permission_level FROM mc_server_users") for row in c.fetchall(): print(f' U: {row[0]} S: {row[1]} Perm: {row[2]}') print("Ready.") @bot.tree.command(name='addserver', description="Add a Minecraft server to the bot") @app_commands.describe( status_url="The Minecraft Minder root URL to query/change server status from, like `http://example.com/minecraft.php?password=1234`", join_url="The Minecraft server hostname and port for people to join, like `minecraft.example.com:25565`" ) async def addserver(interaction: discord.Interaction, status_url: str, join_url: str): logging.info(f'Addserver: {interaction.user.name} {interaction.user.id} added {status_url} {join_url}') # Connect to the database using a context manager with sqlite3.connect('bot_data.db') as conn: c = conn.cursor() c.execute("INSERT OR REPLACE INTO users (id, username) VALUES (?,?)", (interaction.user.id,interaction.user.name)) c.execute("INSERT INTO mc_servers (status_url, join_url) VALUES (?,?)", (status_url,join_url)) server_id = c.lastrowid c.execute("INSERT OR REPLACE INTO mc_server_users (user_id, mc_server_id, permission_level) VALUES (?,?,?)", (interaction.user.id, server_id, 100)) su_id = c.lastrowid conn.commit() logging.warning(f'Addserver: st: {status_url} jo: {join_url} u: {interaction.user.id} s: {server_id} su: {su_id}') await interaction.response.send_message(content="Added!", ephemeral=True) @bot.tree.command(name='adduser', description="Give a user privileges to control your Minecraft server") @app_commands.describe(member="The member you want to add", server_id="The internal server ID to add them to") async def adduser(interaction: discord.Interaction, member: discord.Member, server_id: int): logging.info(f'Adduser: {member.name} {member.id} {server_id}') with sqlite3.connect('bot_data.db') as conn: c = conn.cursor() c.execute("SELECT s.id, su.permission_level FROM mc_servers s JOIN mc_server_users su ON su.user_id = ? AND su.mc_server_id = ? AND su.mc_server_id = s.id", (interaction.user.id,server_id)) row = c.fetchone() if row: if row[1] >= 100: c.execute("INSERT OR REPLACE INTO users (id, username) VALUES (?,?)", (member.id,member.name)) c.execute("SELECT user_id, mc_server_id FROM mc_server_users WHERE user_id = ? AND mc_server_id = ?", (member.id, server_id)) if c.fetchone(): logging.warning(f'Adduser: u: {interaction.user.id} m: {member.name} s: {row[0]} has perm') await interaction.response.send_message(content="{} is already added to the server!".format(member.mention), ephemeral=True) return else: c.execute("INSERT OR REPLACE INTO mc_server_users (user_id, mc_server_id, permission_level) VALUES (?,?,?)", (member.id, row[0], 1)) conn.commit() logging.warning(f'Adduser: u: {interaction.user.id} m: {member.name} s: {row[0]}') await interaction.response.send_message(content="Added {} to the server!".format(member.mention)) return else: logging.error(f'Adduser: u: {interaction.user.id} m: {member.name} s: {server_id} privfail') await interaction.response.send_message(content="You don't have enough privileges to add users to server #{}! ({}<100)".format(server_id, row[1]), ephemeral=True) return else: logging.error(f'Adduser: u: {interaction.user.id} m: {member.name} s: {server_id} permfail') await interaction.response.send_message(content="You don't have permission to access server #{}!".format(server_id), ephemeral=True) return @bot.tree.command(name='removeuser', description="Remove a user from the ability to control your Minecraft server") @app_commands.describe(member="The member you want to remove", server_id="The internal server ID to remove them from") async def removeuser(interaction: discord.Interaction, member: discord.Member, server_id: int): logging.info(f'Removeuser: {member.name} {member.id} {server_id}') with sqlite3.connect('bot_data.db') as conn: c = conn.cursor() c.execute("SELECT s.id, su.permission_level FROM mc_servers s JOIN mc_server_users su ON su.user_id = ? AND su.mc_server_id = ? AND su.mc_server_id = s.id", (interaction.user.id,server_id)) row = c.fetchone() if row: if row[1] >= 100: c.execute("SELECT user_id, mc_server_id FROM mc_server_users WHERE user_id = ? AND mc_server_id = ?", (member.id, server_id)) if c.fetchone() == None: logging.warning(f'Removeuser: u: {interaction.user.id} m: {member.name} s: {row[0]} doesn\'t have perm') await interaction.response.send_message(content="{} is already removed from the server!".format(member.mention), ephemeral=True) return else: c.execute("DELETE FROM mc_server_users WHERE user_id=? AND mc_server_id=?", (member.id, row[0])) conn.commit() logging.warning(f'Removeuser: u: {interaction.user.id} m: {member.name} s: {row[0]}') await interaction.response.send_message(content="Removed {} from the server!".format(member.mention), ephemeral=True) return else: logging.error(f'Removeuser: u: {interaction.user.id} m: {member.name} s: {server_id} privfail') await interaction.response.send_message(content="You don't have enough privileges to remove users from server #{}! ({}<100)".format(server_id, row[1]), ephemeral=True) return else: logging.error(f'Removeuser: u: {interaction.user.id} m: {member.name} s: {server_id} permfail') await interaction.response.send_message(content="You don't have permission to access server #{}!".format(server_id), ephemeral=True) return @bot.tree.command(name='listservers', description="Show the IDs of the MC servers you have access to") async def listservers(interaction: discord.Interaction): logging.info(f'Listservers: {interaction.user.name}') # Connect to the database using a context manager with sqlite3.connect('bot_data.db') as conn: c = conn.cursor() c.execute("SELECT id, status_url, join_url FROM mc_servers s JOIN mc_server_users su ON su.user_id = ?", (interaction.user.id,)) o = "" for row in c.fetchall(): o += '- **ID**: {}\n - **Status URL**: {}\n - **Join URL**: {}\n'.format(row[0], row[1], row[2]) if o == "": o = "No servers." await interaction.response.send_message(content=o, ephemeral=True) @bot.tree.command(name='status', description="Minecraft Status") async def cmd_status(interaction: discord.Interaction): logging.warning(f'Member {interaction.user} queried Minecraft status.') with sqlite3.connect('bot_data.db') as conn: c = conn.cursor() # Todo: select "this guild's/channel's" server not just the first match c.execute("SELECT s.id, su.permission_level, s.status_url, s.join_url FROM mc_servers s JOIN mc_server_users su ON su.user_id = ? AND su.mc_server_id = s.id", (interaction.user.id,)) row = c.fetchone() if row: if row[1] >= 1: logging.warning(f'Member {interaction.user} started Minecraft.') msg = await interaction.response.defer(thinking=True) res = requests.post(row[2], data={"action": "status"}) await interaction.followup.send(content=f'{res.text.capitalize()}\nJoin at `{row[3]}`') @bot.tree.command(name='start', description="Start Minecraft") async def cmd_start(interaction: discord.Interaction): logging.info(f'Start: {interaction.user.name} {interaction.user.id}') with sqlite3.connect('bot_data.db') as conn: c = conn.cursor() # Todo: select "this guild's/channel's" server not just the first match c.execute("SELECT s.id, su.permission_level, s.status_url, s.join_url FROM mc_servers s JOIN mc_server_users su ON su.user_id = ? AND su.mc_server_id = s.id", (interaction.user.id,)) row = c.fetchone() if row: if row[1] >= 1: logging.warning(f'Member {interaction.user} started Minecraft.') msg = await interaction.response.defer(thinking=True) res = requests.post(row[2], data={"action": "on"}) cont=True count=0 while cont: count += 1 time.sleep(5) res = requests.post(row[2], data={"action": "status"}) if "running" in res.text: await interaction.followup.send(content=f'Started: {res.text.capitalize()}\nJoin at `{row[3]}`') cont=False elif count > 10: await interaction.followup.send(content=f'Starting timed out: {res.text.capitalize()}') cont=False return else: logging.error(f'Start: u: {interaction.user.id} m: {interaction.user.name} s: {row[0]} permfail') await interaction.response.send_message(content="You don't have permission to access server #{}!".format(row[0]), ephemeral=True) return else: logging.error(f'Start: u: {interaction.user.id} m: {interaction.user.name} permfail') await interaction.response.send_message(content="You don't have permission to access any servers!", ephemeral=True) return @bot.command(name='99') async def cmd_nine_nine(ctx): brooklyn_99_quotes = [ 'I\'m the human form of the 💯 emoji.', 'Bingpot!', ( 'Cool. Cool cool cool cool cool cool cool, ' 'no doubt no doubt no doubt no doubt.' ), ] response = random.choice(brooklyn_99_quotes) await ctx.send(response) @bot.command(name='shutdown') async def cmd_shutdown(ctx): """Shutdown the bot""" await ctx.send("Shutting down!") sys.exit() @bot.command(name='sync') async def cmd_sync(ctx): synced = await bot.tree.sync() o = f'{len(synced)}/{len(bot.tree.get_commands())} Commands Synced: ({", ".join([str(o.name) for o in synced])})' await ctx.send(o, ephemeral=True) logging.warning(o) @bot.event async def on_member_remove(member): # Log member leave logging.warning(f'Member {member.name} left the server.') ## Runtime ## # Connect to SQLite database with sqlite3.connect('bot_data.db') as conn: c = conn.cursor() # Create table with fields if it doesn't exist already c.execute('''CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, username TEXT )''') c.execute('''CREATE TABLE IF NOT EXISTS mc_servers ( id INTEGER PRIMARY KEY AUTOINCREMENT, status_url TEXT, join_url TEXT )''') c.execute('''CREATE TABLE IF NOT EXISTS mc_server_users ( user_id TEXT, mc_server_id TEXT, permission_level INTEGER, FOREIGN KEY (user_id) REFERENCES users (id), FOREIGN KEY (mc_server_id) REFERENCES mc_servers (id), PRIMARY KEY (user_id, mc_server_id) )''') conn.commit() # Start bot bot.run(TOKEN)