diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..12f7d95 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Selena", + "type": "debugpy", + "request": "launch", + "program": "E:\\Development\\AI Development\\Selena\\main.py", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..b9bf8cb --- /dev/null +++ b/config.py @@ -0,0 +1,22 @@ +from dotenv import load_dotenv +import os + +load_dotenv() + +config = { + 'DISCORD_TOKEN': os.getenv('DISCORD_TOKEN'), + 'GUILD_ID_1': int(os.getenv('DISCORD_GUILD_ID')), + 'GUILD_ID_2': int(os.getenv('DISCORD_GUILD_ID_2')), + 'DISCORD_CHANNEL_ID': int(os.getenv('DISCORD_CHANNEL_ID')), + 'YOUTUBE_API_KEY': os.getenv('YOUTUBE_API_KEY'), + 'TWITCH_CLIENT_ID': os.getenv('TWITCH_CLIENT_ID'), + 'TWITCH_CLIENT_SECRET': os.getenv('TWITCH_CLIENT_SECRET'), + 'BUNGIE_API_KEY': os.getenv('BUNGIE_API_KEY'), + 'OAUTH_URL': os.getenv('OAUTH_URL'), + 'OAUTH_CLIENT_ID': os.getenv('OAUTH_CLIENT_ID'), + 'modules': { + 'music': {'enabled': True}, + 'terms_privacy': {'enabled': True}, + 'data_privacy': {'enabled': True} + } +} diff --git a/main.py b/main.py new file mode 100644 index 0000000..247d25a --- /dev/null +++ b/main.py @@ -0,0 +1,56 @@ +import discord +from config import config +import logging + +logging.basicConfig(level=logging.INFO) + +TOKEN = config['DISCORD_TOKEN'] +GUILD_ID_1 = config['GUILD_ID_1'] +GUILD_ID_2 = config['GUILD_ID_2'] + +intents = discord.Intents.default() +intents.message_content = True + + +class Selena(discord.Client): + def __init__(self): + super().__init__(intents=intents) + self.tree = discord.app_commands.CommandTree(self) + self.xp_module = None # Initialize as None + self.load_modules() + + async def setup_hook(self): + logging.info("Setting up modules...") + await self.tree.sync(guild=discord.Object(id=GUILD_ID_1)) + logging.info(f"Modules setup and commands synchronized to {GUILD_ID_1}") + await self.tree.sync(guild=discord.Object(id=GUILD_ID_2)) + logging.info(f"Modules setup and commands synchronized to {GUILD_ID_2}") + # Call setup_hook for xp_module here + if self.xp_module: + await self.xp_module.setup_hook() + + def load_modules(self): + if config['modules']['music']['enabled']: + from modules.music.music import setup as music_setup + music_setup(self) + logging.info("Music module loaded") + + if config['modules']['terms_privacy']['enabled']: + from modules.terms_privacy.terms_privacy import setup as terms_privacy_setup + terms_privacy_setup(self) + logging.info("Terms and Privacy module loaded") + + if config['modules']['data_privacy']['enabled']: + from modules.data_privacy.data_privacy import setup as data_privacy_setup + data_privacy_setup(self) + logging.info("Data Privacy module loaded") + + +bot = Selena() + + +@bot.event +async def on_ready(): + logging.info(f'{bot.user.name} has connected to Discord!') + +bot.run(TOKEN) diff --git a/modules/admin/data_privacy.py b/modules/admin/data_privacy.py new file mode 100644 index 0000000..e6b3013 --- /dev/null +++ b/modules/admin/data_privacy.py @@ -0,0 +1,51 @@ +import discord +from discord import app_commands +import sqlite3 + + +class DataPrivacy: + def __init__(self, bot): + self.bot = bot + self.db_path = 'data/selena.db' + + async def fetch_user_data(self, user_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("SELECT * FROM user_data WHERE user_id = ?", (user_id,)) + data = cursor.fetchall() + conn.close() + return data + + async def delete_user_data(self, user_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("DELETE FROM user_data WHERE user_id = ?", (user_id,)) + conn.commit() + conn.close() + + def setup(self, tree: app_commands.CommandTree): + @tree.command(name="request_data", description="Request your stored data") + async def request_data_command(interaction: discord.Interaction): + user_id = interaction.user.id + data = await self.fetch_user_data(user_id) + if data: + await interaction.response.send_message(f"Your data: {data}", ephemeral=True) + else: + await interaction.response.send_message("No data found for your user.", ephemeral=True) + + @tree.command(name="delete_data", description="Request deletion of your stored data") + async def delete_data_command(interaction: discord.Interaction): + user_id = interaction.user.id + await self.delete_user_data(user_id) + await interaction.response.send_message("Your data has been deleted.", ephemeral=True) + + if not tree.get_command("request_data"): + tree.add_command(request_data_command) + + if not tree.get_command("delete_data"): + tree.add_command(delete_data_command) + + +def setup(bot): + data_privacy = DataPrivacy(bot) + data_privacy.setup(bot.tree) diff --git a/modules/admin/terms_privacy.py b/modules/admin/terms_privacy.py new file mode 100644 index 0000000..9956464 --- /dev/null +++ b/modules/admin/terms_privacy.py @@ -0,0 +1,73 @@ +import discord +from discord import app_commands +import sqlite3 + + +class TermsPrivacy: + def __init__(self, bot): + self.bot = bot + self.db_path = 'data/selena.db' + self.privacy_policy_url = "https://advtech92.github.io/selena-website/privacy_policy.html" + self.terms_of_service_url = "https://advtech92.github.io/selena-website/terms_of_service.html" + + async def user_opt_out(self, user_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("INSERT INTO opt_out_users (user_id) VALUES (?)", (user_id,)) + conn.commit() + conn.close() + + async def user_opt_in(self, user_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("DELETE FROM opt_out_users WHERE user_id = ?", (user_id,)) + conn.commit() + conn.close() + + async def is_user_opted_out(self, user_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("SELECT 1 FROM opt_out_users WHERE user_id = ?", (user_id,)) + result = cursor.fetchone() + conn.close() + return result is not None + + def setup(self, tree: app_commands.CommandTree): + @tree.command(name="privacy_policy", description="Show the privacy policy") + async def privacy_policy_command(interaction: discord.Interaction): + embed = discord.Embed(title="Privacy Policy", url=self.privacy_policy_url, description="Read our privacy policy.", color=discord.Color.blue()) + await interaction.response.send_message(embed=embed, ephemeral=True) + + @tree.command(name="terms_of_service", description="Show the terms of service") + async def terms_of_service_command(interaction: discord.Interaction): + embed = discord.Embed(title="Terms of Service", url=self.terms_of_service_url, description="Read our terms of service.", color=discord.Color.blue()) + await interaction.response.send_message(embed=embed, ephemeral=True) + + @tree.command(name="opt_out", description="Opt out of using the bot") + async def opt_out_command(interaction: discord.Interaction): + user_id = interaction.user.id + await self.user_opt_out(user_id) + await interaction.response.send_message("You have opted out of using the bot.", ephemeral=True) + + @tree.command(name="opt_in", description="Opt back in to using the bot") + async def opt_in_command(interaction: discord.Interaction): + user_id = interaction.user.id + await self.user_opt_in(user_id) + await interaction.response.send_message("You have opted back in to using the bot.", ephemeral=True) + + if not tree.get_command("privacy_policy"): + tree.add_command(privacy_policy_command) + + if not tree.get_command("terms_of_service"): + tree.add_command(terms_of_service_command) + + if not tree.get_command("opt_out"): + tree.add_command(opt_out_command) + + if not tree.get_command("opt_in"): + tree.add_command(opt_in_command) + + +def setup(bot): + terms_privacy = TermsPrivacy(bot) + terms_privacy.setup(bot.tree) diff --git a/modules/music/music.py b/modules/music/music.py new file mode 100644 index 0000000..9d4da89 --- /dev/null +++ b/modules/music/music.py @@ -0,0 +1,139 @@ +import discord +from discord.ext import commands +from discord import app_commands +from yt_dlp import YoutubeDL +import os +from dotenv import load_dotenv + +load_dotenv() + +YTDL_OPTIONS = { + 'format': 'bestaudio', + 'noplaylist': 'True', +} + +FFMPEG_OPTIONS = { + 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', + 'options': '-vn' +} + +ytdl = YoutubeDL(YTDL_OPTIONS) + + +class Music(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.queue = [] + self.is_playing = False + self.volume = 0.3 + + async def join(self, interaction: discord.Interaction): + channel = interaction.user.voice.channel + if interaction.guild.voice_client is None: + await channel.connect(self_deaf=True) # Join the channel deafened + await interaction.followup.send("Joined the voice channel.") + else: + await interaction.followup.send("Already in a voice channel.") + + async def leave(self, interaction: discord.Interaction): + if interaction.guild.voice_client: + await interaction.guild.voice_client.disconnect() + await interaction.followup.send("Left the voice channel.") + else: + await interaction.followup.send("Not connected to a voice channel.") + + async def play(self, interaction: discord.Interaction, search: str): + if not interaction.guild.voice_client: + await self.join(interaction) + + info = ytdl.extract_info(f"ytsearch:{search}", download=False)['entries'][0] + url = info['url'] + + if interaction.guild.voice_client.is_playing(): + self.queue.append((url, info['title'])) + await interaction.followup.send(f'Queued: {info["title"]}') + else: + source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(url, **FFMPEG_OPTIONS), volume=self.volume) + interaction.guild.voice_client.play(source, after=lambda e: self.bot.loop.create_task(self.play_next(interaction))) + self.is_playing = True + await interaction.followup.send(f'Playing: {info["title"]}') + + async def play_next(self, interaction: discord.Interaction): + if self.queue: + url, title = self.queue.pop(0) + source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(url, **FFMPEG_OPTIONS), volume=self.volume) + interaction.guild.voice_client.play(source, after=lambda e: self.bot.loop.create_task(self.play_next(interaction))) + await interaction.followup.send(f'Playing next: {title}') + else: + self.is_playing = False + + async def pause(self, interaction: discord.Interaction): + if interaction.guild.voice_client.is_playing(): + interaction.guild.voice_client.pause() + await interaction.followup.send("Paused the song.") + else: + await interaction.followup.send("No song is currently playing.") + + async def resume(self, interaction: discord.Interaction): + if interaction.guild.voice_client.is_paused(): + interaction.guild.voice_client.resume() + await interaction.followup.send("Resumed the song.") + else: + await interaction.followup.send("The song is not paused.") + + async def stop(self, interaction: discord.Interaction): + if interaction.guild.voice_client.is_playing(): + interaction.guild.voice_client.stop() + self.queue = [] + await interaction.followup.send("Stopped the song.") + else: + await interaction.followup.send("No song is currently playing.") + + async def set_volume(self, interaction: discord.Interaction, volume: float): + self.volume = volume + if interaction.guild.voice_client and interaction.guild.voice_client.source: + interaction.guild.voice_client.source.volume = self.volume + await interaction.followup.send(f"Volume set to {volume * 100}%.") + else: + await interaction.followup.send("No audio source found.") + + def setup(self, tree: discord.app_commands.CommandTree): + @tree.command(name="join", description="Join the voice channel") + async def join_command(interaction: discord.Interaction): + await interaction.response.defer() + await self.join(interaction) + + @tree.command(name="leave", description="Leave the voice channel") + async def leave_command(interaction: discord.Interaction): + await interaction.response.defer() + await self.leave(interaction) + + @tree.command(name="play", description="Play a song from YouTube") + async def play_command(interaction: discord.Interaction, search: str): + await interaction.response.defer() + await self.play(interaction, search) + + @tree.command(name="pause", description="Pause the current song") + async def pause_command(interaction: discord.Interaction): + await interaction.response.defer() + await self.pause(interaction) + + @tree.command(name="resume", description="Resume the paused song") + async def resume_command(interaction: discord.Interaction): + await interaction.response.defer() + await self.resume(interaction) + + @tree.command(name="stop", description="Stop the current song") + async def stop_command(interaction: discord.Interaction): + await interaction.response.defer() + await self.stop(interaction) + + @tree.command(name="volume", description="Set the volume (0 to 1)") + async def volume_command(interaction: discord.Interaction, volume: float): + await interaction.response.defer() + await self.set_volume(interaction, volume) + + +def setup(bot): + music = Music(bot) + music.setup(bot.tree)