Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7608832807 | ||
|
470fb8463d | ||
|
7c937b6112 | ||
|
1b5587c958 | ||
|
ae3a610b33 | ||
|
b18ab58876 | ||
|
baade2c148 | ||
|
9123144fe2 | ||
|
24629f4c0f | ||
|
39603b1e06 | ||
|
4d9214e378 | ||
|
cc8a1ea630 | ||
|
f6526e00e3 | ||
|
81f4981646 | ||
|
c78912783a | ||
|
3742029410 | ||
|
f5872626a3 | ||
|
15ed750f8b | ||
|
ecea3b93e0 | ||
|
9bdf9e3a10 | ||
|
4828b8fba4 | ||
|
100d9af588 | ||
|
4f3bc7669d | ||
|
965a7d5637 | ||
|
ed66d6438f | ||
|
726ec91a79 | ||
|
29d9ee5432 |
15
.github/workflows/discord_sync.yml
vendored
Normal file
15
.github/workflows/discord_sync.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
name: Discord Webhook
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
git:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Run Discord Webhook
|
||||||
|
uses: johnnyhuy/actions-discord-git-webhook@main
|
||||||
|
with:
|
||||||
|
webhook_url: ${{ secrets.YOUR_DISCORD_WEBHOOK_URL }}
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -160,3 +160,4 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
/selena.db
|
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
26
config.py
Normal file
26
config.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
|
||||||
|
DISCORD_GUILD_ID = os.getenv("DISCORD_GUILD_ID")
|
||||||
|
SPOTIPY_CLIENT_ID = os.getenv("SPOTIPY_CLIENT_ID")
|
||||||
|
SPOTIPY_CLIENT_SECRET = os.getenv("SPOTIPY_CLIENT_SECRET")
|
||||||
|
SPOTIPY_REDIRECT_URI = os.getenv("SPOTIPY_REDIRECT_URI")
|
||||||
|
PLEX_URL = os.getenv("PLEX_URL")
|
||||||
|
PLEX_TOKEN = os.getenv("PLEX_TOKEN")
|
||||||
|
TWITCH_CLIENT_ID = os.getenv("TWITCH_CLIENT_ID")
|
||||||
|
TWITCH_CLIENT_SECRET = os.getenv("TWITCH_CLIENT_SECRET")
|
||||||
|
TWITCH_CHANNEL = os.getenv("TWITCH_CHANNEL")
|
||||||
|
YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY")
|
||||||
|
|
||||||
|
# List of enabled modules
|
||||||
|
ENABLED_MODULES = [
|
||||||
|
"modules.user.birthday_module",
|
||||||
|
"modules.user.xp_module",
|
||||||
|
"modules.money.currency_module",
|
||||||
|
"modules.social.twitch_module",
|
||||||
|
"modules.social.youtube_module",
|
||||||
|
]
|
35
main.py
Normal file
35
main.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
TOKEN = os.getenv('DISCORD_TOKEN')
|
||||||
|
|
||||||
|
# Import the Music module
|
||||||
|
from modules.music import Music
|
||||||
|
|
||||||
|
intents = discord.Intents.default()
|
||||||
|
intents.message_content = True # Required for accessing message content
|
||||||
|
|
||||||
|
|
||||||
|
class Selena(discord.Client):
|
||||||
|
def __init__(self, *, intents):
|
||||||
|
super().__init__(intents=intents)
|
||||||
|
self.tree = discord.app_commands.CommandTree(self)
|
||||||
|
|
||||||
|
# Initialize modules
|
||||||
|
self.music = Music(self)
|
||||||
|
|
||||||
|
async def setup_hook(self):
|
||||||
|
# Sync the app commands with Discord
|
||||||
|
self.loop.create_task(self.music.auto_resume_playback())
|
||||||
|
await self.tree.sync()
|
||||||
|
|
||||||
|
|
||||||
|
client = Selena(intents=intents)
|
||||||
|
|
||||||
|
# Run the bot
|
||||||
|
client.run(TOKEN)
|
0
modules/__init__.py
Normal file
0
modules/__init__.py
Normal file
64
modules/admin/logger_module.py
Normal file
64
modules/admin/logger_module.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# modules/admin/logger_module.py
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
from .logging_config import logging_config
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerModule:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
logging.config.dictConfig(logging_config)
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
self.add_logging_commands()
|
||||||
|
|
||||||
|
def add_logging_commands(self):
|
||||||
|
@self.bot.tree.command(name="log_test", description="Test the logging system") # noqa: E501
|
||||||
|
async def log_test(interaction: discord.Interaction):
|
||||||
|
self.logger.debug("This is a debug message")
|
||||||
|
self.logger.info("This is an info message")
|
||||||
|
self.logger.warning("This is a warning message")
|
||||||
|
self.logger.error("This is an error message")
|
||||||
|
self.logger.critical("This is a critical message")
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Logging test completed. Check the logs!"
|
||||||
|
)
|
||||||
|
|
||||||
|
@self.bot.tree.command(
|
||||||
|
name="set_log_level", description="Set the logging level (Owner/Admin only)" # noqa: E501
|
||||||
|
)
|
||||||
|
@app_commands.choices(
|
||||||
|
level=[
|
||||||
|
app_commands.Choice(name="DEBUG", value="DEBUG"),
|
||||||
|
app_commands.Choice(name="INFO", value="INFO"),
|
||||||
|
app_commands.Choice(name="WARNING", value="WARNING"),
|
||||||
|
app_commands.Choice(name="ERROR", value="ERROR"),
|
||||||
|
app_commands.Choice(name="CRITICAL", value="CRITICAL"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
async def set_log_level(
|
||||||
|
interaction: discord.Interaction, level: app_commands.Choice[str]
|
||||||
|
):
|
||||||
|
guild = interaction.guild
|
||||||
|
if guild is not None and (
|
||||||
|
interaction.user.id == guild.owner_id
|
||||||
|
or any(
|
||||||
|
role.permissions.administrator for role in interaction.user.roles # noqa: E501
|
||||||
|
)
|
||||||
|
):
|
||||||
|
logging.getLogger().setLevel(level.value)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Logging level set to {level.value}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"You do not have permission to set the logging level.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
LoggerModule(bot)
|
36
modules/admin/logging_config.py
Normal file
36
modules/admin/logging_config.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# modules/admin/logging_config.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
LOG_DIR = "logs"
|
||||||
|
if not os.path.exists(LOG_DIR):
|
||||||
|
os.makedirs(LOG_DIR)
|
||||||
|
|
||||||
|
LOG_FILE = os.path.join(LOG_DIR, "selena.log")
|
||||||
|
|
||||||
|
logging_config = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"formatters": {
|
||||||
|
"standard": {"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"},
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "standard",
|
||||||
|
},
|
||||||
|
"file_handler": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"class": "logging.handlers.RotatingFileHandler",
|
||||||
|
"formatter": "standard",
|
||||||
|
"filename": LOG_FILE,
|
||||||
|
"maxBytes": 1024 * 1024 * 5, # 5 MB
|
||||||
|
"backupCount": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"handlers": ["console", "file_handler"],
|
||||||
|
"level": "DEBUG",
|
||||||
|
},
|
||||||
|
}
|
110
modules/admin/policy_module.py
Normal file
110
modules/admin/policy_module.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyModule:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.add_commands()
|
||||||
|
|
||||||
|
def add_commands(self):
|
||||||
|
@app_commands.command(
|
||||||
|
name="privacy_policy", description="View the Privacy Policy"
|
||||||
|
)
|
||||||
|
async def privacy_policy(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
try:
|
||||||
|
privacy_policy_text = """
|
||||||
|
**Privacy Policy**
|
||||||
|
|
||||||
|
1. **Data Collection**
|
||||||
|
- We collect your Discord user ID and messages sent to the bot.
|
||||||
|
- No sensitive personal data is collected.
|
||||||
|
|
||||||
|
2. **Data Usage**
|
||||||
|
- Your data is used to provide and improve the bot's functionality.
|
||||||
|
- We do not share your data with third parties.
|
||||||
|
|
||||||
|
3. **Data Storage**
|
||||||
|
- Data is stored securely on our servers.
|
||||||
|
- Data is retained for as long as necessary to provide our services.
|
||||||
|
|
||||||
|
4. **Your Rights**
|
||||||
|
- You have the right to access, modify, and delete your data.
|
||||||
|
- To exercise these rights, contact the bot admin.
|
||||||
|
|
||||||
|
5. **Changes to Privacy Policy**
|
||||||
|
- We may update this policy from time to time.
|
||||||
|
- You will be notified of any significant changes.
|
||||||
|
|
||||||
|
6. **Contact**
|
||||||
|
- For any questions about this policy, contact the bot admin.
|
||||||
|
"""
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Privacy Policy",
|
||||||
|
description=privacy_policy_text,
|
||||||
|
color=discord.Color.blue(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(f"User {interaction.user.id} viewed the privacy policy")
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in privacy_policy command: {e}")
|
||||||
|
|
||||||
|
@app_commands.command(
|
||||||
|
name="terms_of_service", description="View the Terms of Service"
|
||||||
|
)
|
||||||
|
async def terms_of_service(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
try:
|
||||||
|
tos_text = """
|
||||||
|
**Terms of Service**
|
||||||
|
|
||||||
|
1. **Acceptance of Terms**
|
||||||
|
- By using this bot, you agree to these terms.
|
||||||
|
- If you do not agree, do not use the bot.
|
||||||
|
|
||||||
|
2. **Use of the Bot**
|
||||||
|
- You must follow all applicable laws and regulations.
|
||||||
|
- Do not use the bot for any illegal or unauthorized purpose.
|
||||||
|
|
||||||
|
3. **Changes to Terms**
|
||||||
|
- We may update these terms from time to time.
|
||||||
|
- You will be notified of any significant changes.
|
||||||
|
|
||||||
|
4. **Termination**
|
||||||
|
- We reserve the right to terminate or restrict your access to the bot at any time, without notice or liability.
|
||||||
|
|
||||||
|
5. **Disclaimer of Warranties**
|
||||||
|
- The bot is provided "as is" without warranties of any kind.
|
||||||
|
- We do not guarantee that the bot will be error-free or uninterrupted.
|
||||||
|
|
||||||
|
6. **Limitation of Liability**
|
||||||
|
- We shall not be liable for any damages arising from your use of the bot.
|
||||||
|
|
||||||
|
7. **Contact**
|
||||||
|
- For any questions about these terms, contact the bot admin.
|
||||||
|
"""
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Terms of Service",
|
||||||
|
description=tos_text,
|
||||||
|
color=discord.Color.blue(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(f"User {interaction.user.id} viewed the terms of service")
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in terms_of_service command: {e}")
|
||||||
|
|
||||||
|
self.bot.tree.add_command(privacy_policy)
|
||||||
|
self.bot.tree.add_command(terms_of_service)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
PolicyModule(bot)
|
69
modules/data/db.py
Normal file
69
modules/data/db.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_db():
|
||||||
|
conn = sqlite3.connect("selena.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Birthdays table
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS birthdays (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
birthday TEXT
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Currency table
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS currency (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
balance INTEGER,
|
||||||
|
last_earned TIMESTAMP
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Followed channels table
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS followed_channels (
|
||||||
|
twitch_name TEXT PRIMARY KEY,
|
||||||
|
discord_channel_id INTEGER
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS followed_youtube_channels (
|
||||||
|
youtube_channel_id TEXT PRIMARY KEY,
|
||||||
|
discord_channel_id INTEGER
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS youtube_status (
|
||||||
|
youtube_channel_id TEXT PRIMARY KEY,
|
||||||
|
last_video_id TEXT
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS user_xp (
|
||||||
|
user_id INTEGER PRIMARY KEY,
|
||||||
|
xp INTEGER,
|
||||||
|
level INTEGER
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection():
|
||||||
|
return sqlite3.connect("selena.db")
|
374
modules/media/spotify_module.py
Normal file
374
modules/media/spotify_module.py
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
import logging
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
import spotipy
|
||||||
|
from spotipy.oauth2 import SpotifyOAuth
|
||||||
|
import config
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SpotifyModule:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.user_sessions = {} # To store user-specific Spotify sessions
|
||||||
|
self.auth_managers = {} # To store auth managers for each user
|
||||||
|
self.add_commands()
|
||||||
|
|
||||||
|
def get_spotify_session(self, user_id):
|
||||||
|
return self.user_sessions.get(user_id, None)
|
||||||
|
|
||||||
|
def add_commands(self):
|
||||||
|
@app_commands.command(name="login_spotify", description="Login to Spotify")
|
||||||
|
async def login_spotify(interaction: discord.Interaction):
|
||||||
|
auth_manager = SpotifyOAuth(
|
||||||
|
client_id=config.SPOTIPY_CLIENT_ID,
|
||||||
|
client_secret=config.SPOTIPY_CLIENT_SECRET,
|
||||||
|
redirect_uri=config.SPOTIPY_REDIRECT_URI,
|
||||||
|
scope=(
|
||||||
|
"user-library-read user-read-playback-state "
|
||||||
|
"user-modify-playback-state user-read-currently-playing"
|
||||||
|
),
|
||||||
|
cache_path=f".cache-{interaction.user.id}" # Store tokens per user
|
||||||
|
)
|
||||||
|
auth_url = auth_manager.get_authorize_url()
|
||||||
|
self.auth_managers[interaction.user.id] = auth_manager
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Please log in to Spotify: [Login]({auth_url})\n"
|
||||||
|
"After logging in, please send the `/verify_spotify` command with the URL you were redirected to."
|
||||||
|
)
|
||||||
|
|
||||||
|
@app_commands.command(name="verify_spotify", description="Verify Spotify login")
|
||||||
|
async def verify_spotify(interaction: discord.Interaction, callback_url: str):
|
||||||
|
user_id = interaction.user.id
|
||||||
|
if user_id not in self.auth_managers:
|
||||||
|
await interaction.response.send_message("Please initiate login first using /login_spotify.")
|
||||||
|
return
|
||||||
|
|
||||||
|
auth_manager = self.auth_managers[user_id]
|
||||||
|
try:
|
||||||
|
code = auth_manager.parse_response_code(callback_url)
|
||||||
|
token_info = auth_manager.get_access_token(code)
|
||||||
|
|
||||||
|
if token_info:
|
||||||
|
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||||
|
self.user_sessions[user_id] = sp
|
||||||
|
await interaction.response.send_message("Logged in to Spotify. You can now use Spotify commands.")
|
||||||
|
del self.auth_managers[user_id] # Clean up the used auth manager
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message("Failed to verify Spotify login. Please try again.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error verifying Spotify login: {e}", exc_info=True)
|
||||||
|
await interaction.response.send_message(f"Failed to verify Spotify login: {e}")
|
||||||
|
|
||||||
|
@app_commands.command(name="current_track", description="Get the currently playing song")
|
||||||
|
async def current_track(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
sp = self.get_spotify_session(interaction.user.id)
|
||||||
|
if not sp:
|
||||||
|
await interaction.followup.send("Please log in to Spotify first using /login_spotify.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
current = sp.currently_playing()
|
||||||
|
if current is None or current["item"] is None:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Current Track",
|
||||||
|
description="No song is currently playing",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info("No song is currently playing")
|
||||||
|
else:
|
||||||
|
track = current["item"]
|
||||||
|
artist = ", ".join([a["name"] for a in track["artists"]])
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Current Track",
|
||||||
|
description=f"{track['name']} by {artist}",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
embed.add_field(name="Album", value=track["album"]["name"], inline=False)
|
||||||
|
embed.set_thumbnail(url=track["album"]["images"][0]["url"])
|
||||||
|
await interaction.followup.send(embed=embed)
|
||||||
|
logger.info(f"Currently playing: {track['name']} by {artist}")
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in current_track command: {e}")
|
||||||
|
|
||||||
|
@app_commands.command(name="play_track", description="Play a track by searching for it")
|
||||||
|
async def play_track(interaction: discord.Interaction, query: str):
|
||||||
|
await interaction.response.defer()
|
||||||
|
sp = self.get_spotify_session(interaction.user.id)
|
||||||
|
if not sp:
|
||||||
|
await interaction.followup.send("Please log in to Spotify first using /login_spotify.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
results = sp.search(q=query, limit=1, type="track")
|
||||||
|
if not results["tracks"]["items"]:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Play Track",
|
||||||
|
description="No results found",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(f"No results found for query: {query}")
|
||||||
|
return
|
||||||
|
|
||||||
|
track = results["tracks"]["items"][0]
|
||||||
|
uri = track["uri"]
|
||||||
|
|
||||||
|
devices = sp.devices()
|
||||||
|
if not devices["devices"]:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Play Track",
|
||||||
|
description="No active devices found. Please open Spotify on a device.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info("No active devices found for playback")
|
||||||
|
return
|
||||||
|
|
||||||
|
sp.start_playback(uris=[uri])
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Now Playing",
|
||||||
|
description=f"{track['name']} by {', '.join([a['name'] for a in track['artists']])}",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
embed.add_field(name="Album", value=track["album"]["name"], inline=False)
|
||||||
|
embed.set_thumbnail(url=track["album"]["images"][0]["url"])
|
||||||
|
await interaction.followup.send(embed=embed)
|
||||||
|
logger.info(f"Now playing: {track['name']} by {', '.join([a['name'] for a in track['artists']])}")
|
||||||
|
except spotipy.SpotifyException as e:
|
||||||
|
if e.http_status == 403:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Play Track",
|
||||||
|
description="Permission denied. Please check your Spotify account settings.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.error(f"Permission denied: {e}")
|
||||||
|
else:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in play_track command: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in play_track command: {e}")
|
||||||
|
|
||||||
|
@app_commands.command(name="play_playlist", description="Play a playlist by searching for it or providing a link")
|
||||||
|
async def play_playlist(interaction: discord.Interaction, query: str):
|
||||||
|
await interaction.response.defer()
|
||||||
|
sp = self.get_spotify_session(interaction.user.id)
|
||||||
|
if not sp:
|
||||||
|
await interaction.followup.send("Please log in to Spotify first using /login_spotify.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if query.startswith("https://open.spotify.com/playlist/"):
|
||||||
|
uri = query.split("/")[-1].split("?")[0]
|
||||||
|
uri = f"spotify:playlist:{uri}"
|
||||||
|
else:
|
||||||
|
results = sp.search(q=query, limit=1, type="playlist")
|
||||||
|
if not results["playlists"]["items"]:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Play Playlist",
|
||||||
|
description="No results found",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(f"No results found for query: {query}")
|
||||||
|
return
|
||||||
|
playlist = results["playlists"]["items"][0]
|
||||||
|
uri = playlist["uri"]
|
||||||
|
|
||||||
|
devices = sp.devices()
|
||||||
|
if not devices["devices"]:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Play Playlist",
|
||||||
|
description="No active devices found. Please open Spotify on a device.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info("No active devices found for playback")
|
||||||
|
return
|
||||||
|
|
||||||
|
sp.start_playback(context_uri=uri)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Now Playing Playlist",
|
||||||
|
description=f"{playlist['name']} by {playlist['owner']['display_name']}" if not query.startswith("https://open.spotify.com/playlist/") else "Playing playlist",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
if not query.startswith("https://open.spotify.com/playlist/"):
|
||||||
|
embed.set_thumbnail(url=playlist["images"][0]["url"])
|
||||||
|
await interaction.followup.send(embed=embed)
|
||||||
|
logger.info(f"Now playing playlist: {playlist['name']} by {playlist['owner']['display_name']}")
|
||||||
|
except spotipy.SpotifyException as e:
|
||||||
|
if e.http_status == 403:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Play Playlist",
|
||||||
|
description="Permission denied. Please check your Spotify account settings.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.error(f"Permission denied: {e}")
|
||||||
|
else:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in play_playlist command: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in play_playlist command: {e}")
|
||||||
|
|
||||||
|
@app_commands.command(name="pause", description="Pause the currently playing track")
|
||||||
|
async def pause(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
sp = self.get_spotify_session(interaction.user.id)
|
||||||
|
if not sp:
|
||||||
|
await interaction.followup.send("Please log in to Spotify first using /login_spotify.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
sp.pause_playback()
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Pause",
|
||||||
|
description="Playback paused.",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info("Playback paused")
|
||||||
|
except spotipy.SpotifyException as e:
|
||||||
|
if e.http_status == 403:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Pause",
|
||||||
|
description="Permission denied. Please check your Spotify account settings.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.error(f"Permission denied: {e}")
|
||||||
|
else:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in pause command: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in pause command: {e}")
|
||||||
|
|
||||||
|
@app_commands.command(name="resume", description="Resume the currently playing track")
|
||||||
|
async def resume(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
sp = self.get_spotify_session(interaction.user.id)
|
||||||
|
if not sp:
|
||||||
|
await interaction.followup.send("Please log in to Spotify first using /login_spotify.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
sp.start_playback()
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Resume",
|
||||||
|
description="Playback resumed.",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info("Playback resumed")
|
||||||
|
except spotipy.SpotifyException as e:
|
||||||
|
if e.http_status == 403:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Resume",
|
||||||
|
description="Permission denied. Please check your Spotify account settings.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.error(f"Permission denied: {e}")
|
||||||
|
else:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in resume command: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in resume command: {e}")
|
||||||
|
|
||||||
|
@app_commands.command(name="next", description="Skip to the next track")
|
||||||
|
async def next_track(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
sp = self.get_spotify_session(interaction.user.id)
|
||||||
|
if not sp:
|
||||||
|
await interaction.followup.send("Please log in to Spotify first using /login_spotify.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
sp.next_track()
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Next Track",
|
||||||
|
description="Skipped to the next track.",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info("Skipped to the next track")
|
||||||
|
except spotipy.SpotifyException as e:
|
||||||
|
if e.http_status == 403:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Next Track",
|
||||||
|
description="Permission denied. Please check your Spotify account settings.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.error(f"Permission denied: {e}")
|
||||||
|
else:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in next_track command: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in next_track command: {e}")
|
||||||
|
|
||||||
|
@app_commands.command(name="previous", description="Go back to the previous track")
|
||||||
|
async def previous_track(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
sp = self.get_spotify_session(interaction.user.id)
|
||||||
|
if not sp:
|
||||||
|
await interaction.followup.send("Please log in to Spotify first using /login_spotify.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
sp.previous_track()
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Previous Track",
|
||||||
|
description="Returned to the previous track.",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info("Returned to the previous track")
|
||||||
|
except spotipy.SpotifyException as e:
|
||||||
|
if e.http_status == 403:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Previous Track",
|
||||||
|
description="Permission denied. Please check your Spotify account settings.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.error(f"Permission denied: {e}")
|
||||||
|
else:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in previous_track command: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in previous_track command: {e}")
|
||||||
|
|
||||||
|
self.bot.tree.add_command(login_spotify)
|
||||||
|
self.bot.tree.add_command(verify_spotify)
|
||||||
|
self.bot.tree.add_command(current_track)
|
||||||
|
self.bot.tree.add_command(play_track)
|
||||||
|
self.bot.tree.add_command(play_playlist)
|
||||||
|
self.bot.tree.add_command(pause)
|
||||||
|
self.bot.tree.add_command(resume)
|
||||||
|
self.bot.tree.add_command(next_track)
|
||||||
|
self.bot.tree.add_command(previous_track)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
SpotifyModule(bot)
|
124
modules/money/currency_module.py
Normal file
124
modules/money/currency_module.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# Flake8:noqa: E501
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
from modules.data.db import get_connection, initialize_db
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
initialize_db()
|
||||||
|
|
||||||
|
|
||||||
|
class CurrencyModule:
|
||||||
|
COOLDOWN_PERIOD = timedelta(minutes=5) # Set the cooldown period here
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.add_commands()
|
||||||
|
|
||||||
|
def add_commands(self):
|
||||||
|
@app_commands.command(name="earn_kibble", description="Earn Kibble")
|
||||||
|
async def earn_kibble(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
try:
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
user_id = str(interaction.user.id)
|
||||||
|
|
||||||
|
# Check cooldown
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT last_earned FROM currency WHERE user_id = ?", (user_id,)
|
||||||
|
)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
if result and result[0]:
|
||||||
|
last_earned = datetime.fromisoformat(result[0])
|
||||||
|
if datetime.now() - last_earned < CurrencyModule.COOLDOWN_PERIOD:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Earn Kibble",
|
||||||
|
description="You are on cooldown. Please try again later.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
conn.close()
|
||||||
|
logger.info(
|
||||||
|
f"User {user_id} attempted to earn Kibble but is on cooldown."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
amount = random.choices([random.randint(1, 10), 100], [0.99, 0.01])[0]
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT OR IGNORE INTO currency (user_id, balance, last_earned) VALUES (?, ?, ?)",
|
||||||
|
(user_id, 0, datetime.now().isoformat()),
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE currency SET balance = balance + ?, last_earned = ? WHERE user_id = ?",
|
||||||
|
(amount, datetime.now().isoformat(), user_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT balance FROM currency WHERE user_id = ?", (user_id,)
|
||||||
|
)
|
||||||
|
new_balance = cursor.fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Earn Kibble",
|
||||||
|
description=f"You have earned {amount} Kibble. Your new balance is {new_balance}.",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"User {user_id} earned {amount} Kibble. New balance: {new_balance}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in earn_kibble command: {e}")
|
||||||
|
|
||||||
|
@app_commands.command(
|
||||||
|
name="check_balance", description="Check your Kibble balance"
|
||||||
|
)
|
||||||
|
async def check_balance(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
try:
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT balance FROM currency WHERE user_id = ?",
|
||||||
|
(str(interaction.user.id),),
|
||||||
|
)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
if result:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Check Balance",
|
||||||
|
description=f"Your current balance is {result[0]} Kibble.",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"User {interaction.user.id} checked balance: {result[0]}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Check Balance",
|
||||||
|
description="You have no Kibble.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(f"User {interaction.user.id} has no Kibble.")
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in check_balance command: {e}")
|
||||||
|
|
||||||
|
self.bot.tree.add_command(earn_kibble)
|
||||||
|
self.bot.tree.add_command(check_balance)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
CurrencyModule(bot)
|
378
modules/music.py
Normal file
378
modules/music.py
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
# modules/music.py
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
import yt_dlp
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
class Music:
|
||||||
|
def __init__(self, client):
|
||||||
|
self.client = client
|
||||||
|
self.voice_clients = {}
|
||||||
|
self.music_queues = {}
|
||||||
|
self.current_tracks = {}
|
||||||
|
self.save_file = 'music_state.json'
|
||||||
|
self.volumes = {} # Store volume levels per guild
|
||||||
|
self.default_volume = 0.5 # Default volume level (50%)
|
||||||
|
self.current_interactions = {} # Store interactions per guild
|
||||||
|
|
||||||
|
# Load saved state for auto-resume
|
||||||
|
self.load_music_state()
|
||||||
|
|
||||||
|
# Register app commands
|
||||||
|
self.register_commands()
|
||||||
|
|
||||||
|
# Removed self.client.loop.create_task(self.auto_resume_playback()) from here
|
||||||
|
|
||||||
|
def register_commands(self):
|
||||||
|
@app_commands.command(name='play', description='Play a song by title and artist')
|
||||||
|
async def play(interaction: discord.Interaction, *, query: str):
|
||||||
|
await self.play(interaction, query)
|
||||||
|
|
||||||
|
@app_commands.command(name='pause', description='Pause the current song')
|
||||||
|
async def pause(interaction: discord.Interaction):
|
||||||
|
await self.pause(interaction)
|
||||||
|
|
||||||
|
@app_commands.command(name='resume', description='Resume the paused song')
|
||||||
|
async def resume(interaction: discord.Interaction):
|
||||||
|
await self.resume(interaction)
|
||||||
|
|
||||||
|
@app_commands.command(name='skip', description='Skip the current song')
|
||||||
|
async def skip(interaction: discord.Interaction):
|
||||||
|
await self.skip(interaction)
|
||||||
|
|
||||||
|
@app_commands.command(name='stop', description='Stop playback and clear the queue')
|
||||||
|
async def stop(interaction: discord.Interaction):
|
||||||
|
await self.stop(interaction)
|
||||||
|
|
||||||
|
@app_commands.command(name='leave', description='Disconnect the bot from the voice channel')
|
||||||
|
async def leave(interaction: discord.Interaction):
|
||||||
|
await self.leave(interaction)
|
||||||
|
|
||||||
|
@app_commands.command(name='volume', description='Set the playback volume')
|
||||||
|
@app_commands.describe(level='Volume level between 0 and 100')
|
||||||
|
async def volume(interaction: discord.Interaction, level: int):
|
||||||
|
await self.set_volume(interaction, level)
|
||||||
|
|
||||||
|
# Add commands to the client's tree
|
||||||
|
self.client.tree.add_command(play)
|
||||||
|
self.client.tree.add_command(pause)
|
||||||
|
self.client.tree.add_command(resume)
|
||||||
|
self.client.tree.add_command(skip)
|
||||||
|
self.client.tree.add_command(stop)
|
||||||
|
self.client.tree.add_command(leave)
|
||||||
|
self.client.tree.add_command(volume)
|
||||||
|
|
||||||
|
async def auto_resume_playback(self):
|
||||||
|
await self.client.wait_until_ready()
|
||||||
|
for guild_id_str, url in self.current_tracks.items():
|
||||||
|
guild_id = int(guild_id_str)
|
||||||
|
guild = self.client.get_guild(guild_id)
|
||||||
|
if guild is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Find a voice channel the bot was connected to
|
||||||
|
voice_channel = None
|
||||||
|
for vc in guild.voice_channels:
|
||||||
|
if guild.me in vc.members:
|
||||||
|
voice_channel = vc
|
||||||
|
break
|
||||||
|
|
||||||
|
if voice_channel is None:
|
||||||
|
# If the bot was not in any voice channel, skip this guild
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Connect to the voice channel
|
||||||
|
try:
|
||||||
|
self.voice_clients[guild_id] = await voice_channel.connect()
|
||||||
|
except discord.ClientException:
|
||||||
|
# Already connected to a voice channel in this guild
|
||||||
|
self.voice_clients[guild_id] = guild.voice_client
|
||||||
|
|
||||||
|
# Resume playing
|
||||||
|
await self.play_current_track(guild_id)
|
||||||
|
|
||||||
|
async def play_current_track(self, guild_id):
|
||||||
|
url = self.current_tracks.get(str(guild_id))
|
||||||
|
if not url:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use yt_dlp to get audio source
|
||||||
|
ytdl_opts = {'format': 'bestaudio/best'}
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
with yt_dlp.YoutubeDL(ytdl_opts) as ytdl:
|
||||||
|
info = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=False))
|
||||||
|
audio_url = info['url']
|
||||||
|
title = info.get('title', 'Unknown Title')
|
||||||
|
webpage_url = info.get('webpage_url', url)
|
||||||
|
thumbnail = info.get('thumbnail')
|
||||||
|
|
||||||
|
# Prepare FFmpeg options
|
||||||
|
ffmpeg_opts = {
|
||||||
|
'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
|
||||||
|
'options': '-vn',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create audio source using FFmpegPCMAudio
|
||||||
|
source = discord.FFmpegPCMAudio(audio_url, **ffmpeg_opts)
|
||||||
|
volume = self.volumes.get(str(guild_id), self.default_volume)
|
||||||
|
source = discord.PCMVolumeTransformer(source, volume=volume)
|
||||||
|
|
||||||
|
# Play audio
|
||||||
|
self.voice_clients[guild_id].play(source, after=lambda e: self.after_song(e, guild_id))
|
||||||
|
|
||||||
|
# Send an embedded message indicating the song is now playing
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Auto-Resumed Playing 🎵",
|
||||||
|
description=f"[{title}]({webpage_url})",
|
||||||
|
color=discord.Color.green()
|
||||||
|
)
|
||||||
|
if thumbnail:
|
||||||
|
embed.set_thumbnail(url=thumbnail)
|
||||||
|
|
||||||
|
guild = self.client.get_guild(guild_id)
|
||||||
|
if guild:
|
||||||
|
text_channel = guild.system_channel or guild.text_channels[0]
|
||||||
|
try:
|
||||||
|
await text_channel.send(embed=embed)
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
print(f"Failed to send message: {e}")
|
||||||
|
|
||||||
|
# Save state for auto-resume
|
||||||
|
self.save_music_state()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during auto-resume playback: {e}")
|
||||||
|
if guild_id in self.voice_clients and self.voice_clients[guild_id].is_connected():
|
||||||
|
await self.voice_clients[guild_id].disconnect()
|
||||||
|
|
||||||
|
async def play(self, interaction: discord.Interaction, query: str):
|
||||||
|
await interaction.response.defer()
|
||||||
|
|
||||||
|
guild_id = interaction.guild_id
|
||||||
|
|
||||||
|
# Store the interaction object for later use
|
||||||
|
self.current_interactions[guild_id] = interaction
|
||||||
|
|
||||||
|
# Check if the user is in a voice channel
|
||||||
|
if interaction.user.voice is None:
|
||||||
|
await interaction.followup.send("You must be connected to a voice channel to use this command.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Connect to the voice channel if not already connected
|
||||||
|
if guild_id not in self.voice_clients or not self.voice_clients[guild_id].is_connected():
|
||||||
|
channel = interaction.user.voice.channel
|
||||||
|
self.voice_clients[guild_id] = await channel.connect()
|
||||||
|
|
||||||
|
# Ensure volume is set
|
||||||
|
if guild_id not in self.volumes:
|
||||||
|
self.volumes[str(guild_id)] = self.default_volume
|
||||||
|
|
||||||
|
await interaction.followup.send(f"🔍 **Searching for:** {query}")
|
||||||
|
|
||||||
|
# Search YouTube for the song
|
||||||
|
search_url = await self.search_youtube(query)
|
||||||
|
if not search_url:
|
||||||
|
await interaction.followup.send("❌ **Could not find the song on YouTube.**")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add URL to the music queue
|
||||||
|
if guild_id not in self.music_queues:
|
||||||
|
self.music_queues[guild_id] = []
|
||||||
|
self.music_queues[guild_id].append(search_url)
|
||||||
|
|
||||||
|
await interaction.followup.send(f"✅ **Added to queue:** {query}")
|
||||||
|
|
||||||
|
# If nothing is playing, start playing
|
||||||
|
if not self.voice_clients[guild_id].is_playing():
|
||||||
|
await self.play_next(guild_id)
|
||||||
|
|
||||||
|
async def search_youtube(self, query):
|
||||||
|
"""Searches YouTube for the query and returns the URL of the first result."""
|
||||||
|
ytdl_opts = {
|
||||||
|
'format': 'bestaudio/best',
|
||||||
|
'noplaylist': True,
|
||||||
|
'default_search': 'ytsearch',
|
||||||
|
'quiet': True,
|
||||||
|
'no_warnings': True,
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
with yt_dlp.YoutubeDL(ytdl_opts) as ytdl:
|
||||||
|
try:
|
||||||
|
info = await loop.run_in_executor(None, lambda: ytdl.extract_info(query, download=False))
|
||||||
|
if 'entries' in info:
|
||||||
|
# Take the first item from the search results
|
||||||
|
video = info['entries'][0]
|
||||||
|
else:
|
||||||
|
video = info
|
||||||
|
video_url = f"https://www.youtube.com/watch?v={video['id']}"
|
||||||
|
return video_url
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error searching YouTube: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def play_next(self, guild_id):
|
||||||
|
if guild_id not in self.music_queues or not self.music_queues[guild_id]:
|
||||||
|
# Do not disconnect here
|
||||||
|
return
|
||||||
|
|
||||||
|
url = self.music_queues[guild_id].pop(0)
|
||||||
|
self.current_tracks[str(guild_id)] = url
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use yt_dlp to get audio source
|
||||||
|
ytdl_opts = {'format': 'bestaudio/best'}
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
with yt_dlp.YoutubeDL(ytdl_opts) as ytdl:
|
||||||
|
info = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=False))
|
||||||
|
audio_url = info['url']
|
||||||
|
title = info.get('title', 'Unknown Title')
|
||||||
|
webpage_url = info.get('webpage_url', url)
|
||||||
|
thumbnail = info.get('thumbnail')
|
||||||
|
|
||||||
|
# Prepare FFmpeg options
|
||||||
|
ffmpeg_opts = {
|
||||||
|
'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
|
||||||
|
'options': '-vn',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create audio source using FFmpegPCMAudio
|
||||||
|
source = discord.FFmpegPCMAudio(audio_url, **ffmpeg_opts)
|
||||||
|
volume = self.volumes.get(str(guild_id), self.default_volume)
|
||||||
|
source = discord.PCMVolumeTransformer(source, volume=volume)
|
||||||
|
|
||||||
|
# Play audio
|
||||||
|
self.voice_clients[guild_id].play(source, after=lambda e: self.after_song(e, guild_id))
|
||||||
|
|
||||||
|
# Send an embedded message indicating the song is now playing
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Now Playing 🎵",
|
||||||
|
description=f"[{title}]({webpage_url})",
|
||||||
|
color=discord.Color.blue()
|
||||||
|
)
|
||||||
|
if thumbnail:
|
||||||
|
embed.set_thumbnail(url=thumbnail)
|
||||||
|
|
||||||
|
# Use the stored interaction to send the message
|
||||||
|
interaction = self.current_interactions.get(guild_id)
|
||||||
|
if interaction:
|
||||||
|
try:
|
||||||
|
await interaction.followup.send(embed=embed)
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
print(f"Failed to send message: {e}")
|
||||||
|
finally:
|
||||||
|
# Remove the interaction to prevent reuse
|
||||||
|
self.current_interactions.pop(guild_id, None)
|
||||||
|
else:
|
||||||
|
# Fallback to a default text channel
|
||||||
|
guild = self.client.get_guild(guild_id)
|
||||||
|
if guild:
|
||||||
|
text_channel = guild.system_channel or guild.text_channels[0]
|
||||||
|
try:
|
||||||
|
await text_channel.send(embed=embed)
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
print(f"Failed to send message: {e}")
|
||||||
|
|
||||||
|
# Save state for auto-resume
|
||||||
|
self.save_music_state()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during playback: {e}")
|
||||||
|
if guild_id in self.voice_clients and self.voice_clients[guild_id].is_connected():
|
||||||
|
await self.voice_clients[guild_id].disconnect()
|
||||||
|
|
||||||
|
def after_song(self, error, guild_id):
|
||||||
|
if error:
|
||||||
|
print(f"Error: {error}")
|
||||||
|
coro = self.play_next(guild_id)
|
||||||
|
asyncio.run_coroutine_threadsafe(coro, self.client.loop)
|
||||||
|
|
||||||
|
async def pause(self, interaction: discord.Interaction):
|
||||||
|
guild_id = interaction.guild_id
|
||||||
|
if guild_id in self.voice_clients and self.voice_clients[guild_id].is_playing():
|
||||||
|
self.voice_clients[guild_id].pause()
|
||||||
|
await interaction.response.send_message("⏸️ **Music paused.**")
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message("❌ **No music is playing.**")
|
||||||
|
|
||||||
|
async def resume(self, interaction: discord.Interaction):
|
||||||
|
guild_id = interaction.guild_id
|
||||||
|
if guild_id in self.voice_clients and self.voice_clients[guild_id].is_paused():
|
||||||
|
self.voice_clients[guild_id].resume()
|
||||||
|
await interaction.response.send_message("▶️ **Music resumed.**")
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message("❌ **No music is paused.**")
|
||||||
|
|
||||||
|
async def skip(self, interaction: discord.Interaction):
|
||||||
|
guild_id = interaction.guild_id
|
||||||
|
if guild_id in self.voice_clients and self.voice_clients[guild_id].is_playing():
|
||||||
|
self.voice_clients[guild_id].stop()
|
||||||
|
await interaction.response.send_message("⏭️ **Skipped current song.**")
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message("❌ **No music is playing.**")
|
||||||
|
|
||||||
|
async def stop(self, interaction: discord.Interaction):
|
||||||
|
guild_id = interaction.guild_id
|
||||||
|
if guild_id in self.voice_clients:
|
||||||
|
self.voice_clients[guild_id].stop()
|
||||||
|
self.music_queues[guild_id] = []
|
||||||
|
self.current_tracks.pop(str(guild_id), None) # Remove current track
|
||||||
|
await interaction.response.send_message("🛑 **Playback stopped and queue cleared.**")
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message("❌ **No music is playing.**")
|
||||||
|
|
||||||
|
async def leave(self, interaction: discord.Interaction):
|
||||||
|
guild_id = interaction.guild_id
|
||||||
|
if guild_id in self.voice_clients and self.voice_clients[guild_id].is_connected():
|
||||||
|
await self.voice_clients[guild_id].disconnect()
|
||||||
|
await interaction.response.send_message("👋 **Disconnected from the voice channel.**")
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message("❌ **I am not connected to a voice channel.**")
|
||||||
|
|
||||||
|
async def set_volume(self, interaction: discord.Interaction, level: int):
|
||||||
|
guild_id = interaction.guild_id
|
||||||
|
|
||||||
|
# Validate volume level
|
||||||
|
if level < 0 or level > 100:
|
||||||
|
await interaction.response.send_message("❌ **Volume must be between 0 and 100.**")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set the volume
|
||||||
|
volume = level / 100 # Convert to a 0.0 - 1.0 scale
|
||||||
|
self.volumes[str(guild_id)] = volume
|
||||||
|
|
||||||
|
# Adjust volume if something is playing
|
||||||
|
if guild_id in self.voice_clients and self.voice_clients[guild_id].is_playing():
|
||||||
|
current_source = self.voice_clients[guild_id].source
|
||||||
|
if isinstance(current_source, discord.PCMVolumeTransformer):
|
||||||
|
current_source.volume = volume
|
||||||
|
else:
|
||||||
|
# Wrap the existing source with PCMVolumeTransformer
|
||||||
|
self.voice_clients[guild_id].source = discord.PCMVolumeTransformer(current_source, volume=volume)
|
||||||
|
|
||||||
|
await interaction.response.send_message(f"🔊 **Volume set to {level}%.**")
|
||||||
|
|
||||||
|
def save_music_state(self):
|
||||||
|
state = {
|
||||||
|
'current_tracks': self.current_tracks,
|
||||||
|
'music_queues': self.music_queues,
|
||||||
|
'volumes': self.volumes
|
||||||
|
}
|
||||||
|
with open(self.save_file, 'w') as f:
|
||||||
|
json.dump(state, f)
|
||||||
|
|
||||||
|
def load_music_state(self):
|
||||||
|
if os.path.exists(self.save_file):
|
||||||
|
with open(self.save_file, 'r') as f:
|
||||||
|
state = json.load(f)
|
||||||
|
self.current_tracks = state.get('current_tracks', {})
|
||||||
|
self.music_queues = state.get('music_queues', {})
|
||||||
|
self.volumes = state.get('volumes', {})
|
||||||
|
else:
|
||||||
|
self.current_tracks = {}
|
||||||
|
self.music_queues = {}
|
||||||
|
self.volumes = {}
|
201
modules/social/twitch_module.py
Normal file
201
modules/social/twitch_module.py
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
from twitchAPI.helper import first
|
||||||
|
from twitchAPI.twitch import Twitch
|
||||||
|
|
||||||
|
import config
|
||||||
|
from modules.data.db import get_connection
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TwitchModule:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.twitch = Twitch(config.TWITCH_CLIENT_ID, config.TWITCH_CLIENT_SECRET)
|
||||||
|
self.bot.loop.create_task(self.authenticate_twitch())
|
||||||
|
self.bot.loop.create_task(self.check_live_streams())
|
||||||
|
self.add_commands()
|
||||||
|
|
||||||
|
async def authenticate_twitch(self):
|
||||||
|
await self.twitch.authenticate_app([]) # Authenticate without scopes
|
||||||
|
|
||||||
|
async def check_live_streams(self):
|
||||||
|
while True:
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS live_status (
|
||||||
|
twitch_name TEXT PRIMARY KEY,
|
||||||
|
is_live BOOLEAN
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
c.execute("SELECT twitch_name, discord_channel_id FROM followed_channels")
|
||||||
|
followed_channels = c.fetchall()
|
||||||
|
|
||||||
|
for twitch_name, discord_channel_id in followed_channels:
|
||||||
|
try:
|
||||||
|
user_info = await first(self.twitch.get_users(logins=[twitch_name]))
|
||||||
|
if not user_info:
|
||||||
|
continue
|
||||||
|
|
||||||
|
user_id = user_info.id
|
||||||
|
streams = await first(self.twitch.get_streams(user_id=[user_id]))
|
||||||
|
is_live = streams is not None
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
"SELECT is_live FROM live_status WHERE twitch_name = ?",
|
||||||
|
(twitch_name,),
|
||||||
|
)
|
||||||
|
row = c.fetchone()
|
||||||
|
was_live = row[0] if row else False
|
||||||
|
|
||||||
|
if is_live and not was_live:
|
||||||
|
channel = self.bot.get_channel(discord_channel_id)
|
||||||
|
if channel:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{twitch_name} is Live!",
|
||||||
|
description=(
|
||||||
|
f"**Title:** {streams.title}\n"
|
||||||
|
f"**Game:** {streams.game_name}\n"
|
||||||
|
f"**Viewers:** {streams.viewer_count}"
|
||||||
|
),
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
embed.set_thumbnail(
|
||||||
|
url=streams.thumbnail_url.replace(
|
||||||
|
"{width}", "320"
|
||||||
|
).replace("{height}", "180")
|
||||||
|
)
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
"INSERT OR REPLACE INTO live_status (twitch_name, is_live) VALUES (?, ?)",
|
||||||
|
(twitch_name, is_live),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error checking live status for {twitch_name}: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
await asyncio.sleep(300) # Check every 5 minutes
|
||||||
|
|
||||||
|
def add_commands(self):
|
||||||
|
@app_commands.command(
|
||||||
|
name="follow_twitch",
|
||||||
|
description="Follow a Twitch channel to get live alerts",
|
||||||
|
)
|
||||||
|
async def follow_twitch(
|
||||||
|
interaction: discord.Interaction,
|
||||||
|
twitch_name: str,
|
||||||
|
channel: discord.TextChannel = None,
|
||||||
|
):
|
||||||
|
channel = channel or interaction.channel
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"INSERT OR REPLACE INTO followed_channels (twitch_name, discord_channel_id) VALUES (?, ?)",
|
||||||
|
(twitch_name, channel.id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
await interaction.response.send_message(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Followed Twitch Channel",
|
||||||
|
description=f"Now following {twitch_name}. Alerts will be sent to {channel.mention}.",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(f"Now following {twitch_name} for alerts in {channel.name}")
|
||||||
|
|
||||||
|
@app_commands.command(
|
||||||
|
name="unfollow_twitch", description="Unfollow a Twitch channel"
|
||||||
|
)
|
||||||
|
async def unfollow_twitch(interaction: discord.Interaction, twitch_name: str):
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"DELETE FROM followed_channels WHERE twitch_name = ?", (twitch_name,)
|
||||||
|
)
|
||||||
|
c.execute("DELETE FROM live_status WHERE twitch_name = ?", (twitch_name,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
await interaction.response.send_message(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Unfollowed Twitch Channel",
|
||||||
|
description=f"No longer following {twitch_name}.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(f"No longer following {twitch_name}")
|
||||||
|
|
||||||
|
@app_commands.command(
|
||||||
|
name="twitch_live", description="Check if a Twitch streamer is live"
|
||||||
|
)
|
||||||
|
async def twitch_live(interaction: discord.Interaction, streamer: str):
|
||||||
|
await interaction.response.defer()
|
||||||
|
try:
|
||||||
|
logger.debug(f"Fetching user info for streamer: {streamer}")
|
||||||
|
user_info = await first(self.twitch.get_users(logins=[streamer]))
|
||||||
|
if not user_info:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Twitch Live Check",
|
||||||
|
description=f"Streamer {streamer} not found.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(f"Streamer {streamer} not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
user_id = user_info.id
|
||||||
|
logger.debug(f"Fetching stream info for user ID: {user_id}")
|
||||||
|
streams = await first(self.twitch.get_streams(user_id=[user_id]))
|
||||||
|
if streams:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{streamer} is Live!",
|
||||||
|
description=(
|
||||||
|
f"**Title:** {streams.title}\n"
|
||||||
|
f"**Game:** {streams.game_name}\n"
|
||||||
|
f"**Viewers:** {streams.viewer_count}"
|
||||||
|
),
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
embed.set_thumbnail(
|
||||||
|
url=streams.thumbnail_url.replace("{width}", "320").replace(
|
||||||
|
"{height}", "180"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await interaction.followup.send(embed=embed)
|
||||||
|
logger.info(f"Streamer {streamer} is live.")
|
||||||
|
else:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title=f"{streamer} is not live",
|
||||||
|
description=f"{streamer} is currently offline.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(f"Streamer {streamer} is offline.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in twitch_live command: {e}", exc_info=True)
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
|
||||||
|
self.bot.tree.add_command(follow_twitch)
|
||||||
|
self.bot.tree.add_command(unfollow_twitch)
|
||||||
|
self.bot.tree.add_command(twitch_live)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
TwitchModule(bot)
|
148
modules/social/youtube_module.py
Normal file
148
modules/social/youtube_module.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# modules/social/youtube_module.py
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
|
||||||
|
import config
|
||||||
|
from modules.data.db import get_connection
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class YouTubeModule:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.youtube = build("youtube", "v3", developerKey=config.YOUTUBE_API_KEY)
|
||||||
|
self.bot.loop.create_task(self.check_youtube_channels())
|
||||||
|
self.add_commands()
|
||||||
|
|
||||||
|
async def check_youtube_channels(self):
|
||||||
|
while True:
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"SELECT youtube_channel_id, discord_channel_id FROM followed_youtube_channels"
|
||||||
|
)
|
||||||
|
followed_channels = c.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
for youtube_channel_id, discord_channel_id in followed_channels:
|
||||||
|
try:
|
||||||
|
request = self.youtube.channels().list(
|
||||||
|
part="contentDetails", id=youtube_channel_id
|
||||||
|
)
|
||||||
|
response = request.execute()
|
||||||
|
if "items" in response and len(response["items"]) > 0:
|
||||||
|
uploads_playlist_id = response["items"][0]["contentDetails"][
|
||||||
|
"relatedPlaylists"
|
||||||
|
]["uploads"]
|
||||||
|
|
||||||
|
request = self.youtube.playlistItems().list(
|
||||||
|
part="snippet", playlistId=uploads_playlist_id, maxResults=1
|
||||||
|
)
|
||||||
|
response = request.execute()
|
||||||
|
if "items" in response and len(response["items"]) > 0:
|
||||||
|
latest_video = response["items"][0]["snippet"]
|
||||||
|
video_id = latest_video["resourceId"]["videoId"]
|
||||||
|
title = latest_video["title"]
|
||||||
|
latest_video["publishedAt"]
|
||||||
|
thumbnail_url = latest_video["thumbnails"]["high"]["url"]
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
"SELECT last_video_id FROM youtube_status WHERE youtube_channel_id = ?",
|
||||||
|
(youtube_channel_id,),
|
||||||
|
)
|
||||||
|
row = c.fetchone()
|
||||||
|
last_video_id = row[0] if row else None
|
||||||
|
|
||||||
|
if video_id != last_video_id:
|
||||||
|
channel = self.bot.get_channel(discord_channel_id)
|
||||||
|
if channel:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"New Video from {youtube_channel_id}",
|
||||||
|
description=f"{title}\n[Watch now](https://www.youtube.com/watch?v={video_id})",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
embed.set_thumbnail(url=thumbnail_url)
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
"INSERT OR REPLACE INTO youtube_status (youtube_channel_id, last_video_id) VALUES (?, ?)",
|
||||||
|
(youtube_channel_id, video_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error checking YouTube channel {youtube_channel_id}: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
await asyncio.sleep(300) # Check every 5 minutes
|
||||||
|
|
||||||
|
def add_commands(self):
|
||||||
|
@app_commands.command(
|
||||||
|
name="follow_youtube",
|
||||||
|
description="Follow a YouTube channel to get video updates",
|
||||||
|
)
|
||||||
|
async def follow_youtube(
|
||||||
|
interaction: discord.Interaction,
|
||||||
|
youtube_channel_id: str,
|
||||||
|
channel: discord.TextChannel = None,
|
||||||
|
):
|
||||||
|
channel = channel or interaction.channel
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"INSERT OR REPLACE INTO followed_youtube_channels (youtube_channel_id, discord_channel_id) VALUES (?, ?)",
|
||||||
|
(youtube_channel_id, channel.id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
await interaction.response.send_message(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Followed YouTube Channel",
|
||||||
|
description=f"Now following {youtube_channel_id}. Alerts will be sent to {channel.mention}.",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Now following {youtube_channel_id} for video updates in {channel.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app_commands.command(
|
||||||
|
name="unfollow_youtube", description="Unfollow a YouTube channel"
|
||||||
|
)
|
||||||
|
async def unfollow_youtube(
|
||||||
|
interaction: discord.Interaction, youtube_channel_id: str
|
||||||
|
):
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"DELETE FROM followed_youtube_channels WHERE youtube_channel_id = ?",
|
||||||
|
(youtube_channel_id,),
|
||||||
|
)
|
||||||
|
c.execute(
|
||||||
|
"DELETE FROM youtube_status WHERE youtube_channel_id = ?",
|
||||||
|
(youtube_channel_id,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
await interaction.response.send_message(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Unfollowed YouTube Channel",
|
||||||
|
description=f"No longer following {youtube_channel_id}.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(f"No longer following {youtube_channel_id}")
|
||||||
|
|
||||||
|
self.bot.tree.add_command(follow_youtube)
|
||||||
|
self.bot.tree.add_command(unfollow_youtube)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
YouTubeModule(bot)
|
111
modules/user/birthday_module.py
Normal file
111
modules/user/birthday_module.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# Flake8:noqa: E501
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
from modules.data.db import get_connection, initialize_db
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
initialize_db()
|
||||||
|
|
||||||
|
|
||||||
|
class BirthdayModule:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.add_commands()
|
||||||
|
|
||||||
|
def add_commands(self):
|
||||||
|
@app_commands.command(name="add_birthday", description="Add your birthday")
|
||||||
|
async def add_birthday(interaction: discord.Interaction, date: str):
|
||||||
|
await interaction.response.defer()
|
||||||
|
try:
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT OR REPLACE INTO birthdays (user_id, birthday) VALUES (?, ?)",
|
||||||
|
(str(interaction.user.id), date),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Add Birthday",
|
||||||
|
description=f"Your birthday {date} has been added.",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(f"Birthday added for user {interaction.user.id}: {date}")
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in add_birthday command: {e}")
|
||||||
|
|
||||||
|
@app_commands.command(name="view_birthday", description="View your birthday")
|
||||||
|
async def view_birthday(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
try:
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT birthday FROM birthdays WHERE user_id = ?",
|
||||||
|
(str(interaction.user.id),),
|
||||||
|
)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
if result:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="View Birthday",
|
||||||
|
description=f"Your birthday is {result[0]}.",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Birthday viewed for user {interaction.user.id}: {result[0]}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="View Birthday",
|
||||||
|
description="You have not set a birthday yet.",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(f"Birthday not found for user {interaction.user.id}")
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in view_birthday command: {e}")
|
||||||
|
|
||||||
|
@app_commands.command(
|
||||||
|
name="remove_birthday", description="Remove your birthday"
|
||||||
|
)
|
||||||
|
async def remove_birthday(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
try:
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM birthdays WHERE user_id = ?",
|
||||||
|
(str(interaction.user.id),),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Remove Birthday",
|
||||||
|
description="Your birthday has been removed.",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.info(f"Birthday removed for user {interaction.user.id}")
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(f"An error occurred: {e}")
|
||||||
|
logger.error(f"Error in remove_birthday command: {e}")
|
||||||
|
|
||||||
|
self.bot.tree.add_command(add_birthday)
|
||||||
|
self.bot.tree.add_command(view_birthday)
|
||||||
|
self.bot.tree.add_command(remove_birthday)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
BirthdayModule(bot)
|
101
modules/user/xp_module.py
Normal file
101
modules/user/xp_module.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import logging
|
||||||
|
import random
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
from modules.data.db import get_connection
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class XPModule:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.cooldown = {} # Dictionary to store cooldowns
|
||||||
|
self.add_commands()
|
||||||
|
self.setup_event_listeners()
|
||||||
|
|
||||||
|
async def on_message(self, message):
|
||||||
|
logger.debug(f"Received message from {message.author.id}")
|
||||||
|
if message.author.bot:
|
||||||
|
logger.debug("Message is from a bot, ignoring")
|
||||||
|
return
|
||||||
|
|
||||||
|
user_id = message.author.id
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
if user_id in self.cooldown and now < self.cooldown[user_id]:
|
||||||
|
logger.debug(f"User {user_id} is on cooldown, ignoring")
|
||||||
|
return # User is on cooldown
|
||||||
|
|
||||||
|
xp = random.randint(1, 5) # Award between 1 and 5 XP for a message
|
||||||
|
logger.debug(f"Awarding {xp} XP to user {user_id}")
|
||||||
|
self.give_xp(user_id, xp)
|
||||||
|
self.cooldown[user_id] = now + timedelta(seconds=10) # 1 minute cooldown # noqa: E501
|
||||||
|
|
||||||
|
def give_xp(self, user_id, xp):
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("SELECT xp, level FROM user_xp WHERE user_id = ?", (user_id,)) # noqa: E501
|
||||||
|
row = c.fetchone()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
current_xp, current_level = row
|
||||||
|
new_xp = current_xp + xp
|
||||||
|
new_level = current_level
|
||||||
|
|
||||||
|
# Level up logic
|
||||||
|
while new_xp >= self.xp_for_next_level(new_level):
|
||||||
|
new_xp -= self.xp_for_next_level(new_level)
|
||||||
|
new_level += 1
|
||||||
|
logger.info(f"User {user_id} leveled up to {new_level}")
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
"UPDATE user_xp SET xp = ?, level = ? WHERE user_id = ?",
|
||||||
|
(new_xp, new_level, user_id),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
c.execute(
|
||||||
|
"INSERT INTO user_xp (user_id, xp, level) VALUES (?, ?, ?)",
|
||||||
|
(user_id, xp, 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
logger.debug(f"Updated XP for user {user_id}")
|
||||||
|
|
||||||
|
def xp_for_next_level(self, level):
|
||||||
|
return int(
|
||||||
|
100 * (1.5 ** (level - 1))
|
||||||
|
) # Exponential scaling for XP required to level up
|
||||||
|
|
||||||
|
def add_commands(self):
|
||||||
|
@app_commands.command(name="xp", description="Check your XP and level")
|
||||||
|
async def check_xp(interaction: discord.Interaction):
|
||||||
|
user_id = interaction.user.id
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("SELECT xp, level FROM user_xp WHERE user_id = ?", (user_id,)) # noqa: E501
|
||||||
|
row = c.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
xp, level = row
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"You have {xp} XP and are level {level}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message("You have no XP yet.")
|
||||||
|
|
||||||
|
self.bot.tree.add_command(check_xp)
|
||||||
|
|
||||||
|
def setup_event_listeners(self):
|
||||||
|
@self.bot.event
|
||||||
|
async def on_message(message):
|
||||||
|
await self.on_message(message)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
XPModule(bot)
|
1
music_state.json
Normal file
1
music_state.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"current_tracks": {"1161168803888107550": "https://www.youtube.com/watch?v=fnlJw9H0xAM", "1142517462529757184": "https://www.youtube.com/watch?v=wWoQ7PFSYlk"}, "music_queues": {"1161168803888107550": [], "1142517462529757184": []}, "volumes": {"1161168803888107550": 0.5, "1142517462529757184": 0.5}}
|
Loading…
x
Reference in New Issue
Block a user