Skullbot/skullbot.py
2025-09-20 08:26:31 +02:00

345 lines
11 KiB
Python

"""
Discord + Telegram Event Reminder & Music Bot
--------------------------------------------
By Kevin Heyer, Skull-IT.de
Features:
- Stores Discord Scheduled Events in a SQLite DB
- Sends notifications about new events to Discord and Telegram
- Sends reminders 1 day before an event starts
- Can join a voice channel and play YouTube audio (music bot)
- Uses yt-dlp + FFmpeg for audio streaming
Requirements:
- discord.py
- python-telegram-bot (or telegram)
- yt-dlp
- ffmpeg installed on the system
- PyNaCl installed (for Discord voice support)
sudo apt-get update
sudo apt-get install -y ffmpeg python3-dev libffi-dev libnacl-dev build-essential
pip install -r requirements.txt
Make sure to configure your tokens and IDs inside a .env file.
"""
# =====================
# IMPORTS
# =====================
import os
import sqlite3
import asyncio
from datetime import datetime, timedelta, timezone
import discord
from discord.ext import commands, tasks
from discord.ui import Button, View
from telegram import Bot as TelegramBot
import yt_dlp as youtube_dl
from dotenv import load_dotenv
# =====================
# LOAD ENVIRONMENT VARIABLES
# =====================
load_dotenv()
BOT_TOKEN = os.getenv("BOT_TOKEN")
DISCORD_CHANNEL_ID = os.getenv("DISCORD_CHANNEL_ID")
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
# =====================
# DISCORD SETUP
# =====================
intents = discord.Intents.default()
intents.guild_scheduled_events = True
intents.messages = True
intents.message_content = True # Needed for text commands
bot = commands.Bot(command_prefix="!", intents=intents)
# =====================
# TELEGRAM BOT
# =====================
tg_bot = TelegramBot(token=TELEGRAM_TOKEN)
# =====================
# DATABASE SETUP
# =====================
conn = sqlite3.connect("events.db")
c = conn.cursor()
c.execute(
"""CREATE TABLE IF NOT EXISTS events (
id TEXT PRIMARY KEY,
name TEXT,
start_time TEXT,
reminded INTEGER DEFAULT 0
)"""
)
conn.commit()
# =====================
# DISCORD EVENT HANDLER
# =====================
@bot.event
async def on_scheduled_event_create(event: discord.ScheduledEvent):
"""
Triggered when a new scheduled event is created on the server.
Saves the event to the database and sends notifications.
"""
# Store event in the database
c.execute(
"INSERT OR IGNORE INTO events (id, name, start_time) VALUES (?, ?, ?)",
(str(event.id), event.name, event.start_time.isoformat()),
)
conn.commit()
# Send Discord notification
channel = bot.get_channel(DISCORD_CHANNEL_ID)
if channel:
await channel.send(f"📅 New Event Created: **{event.name}** at {event.start_time}")
# Send Telegram notification
await tg_bot.send_message(
chat_id=TELEGRAM_CHAT_ID,
text=f"📅 New Event Created: {event.name} at {event.start_time}",
)
# =====================
# REMINDER TASK
# =====================
@tasks.loop(minutes=60)
async def reminder_task():
"""
Runs every hour, checks if any event starts within 24 hours,
and sends a reminder message to Discord and Telegram.
"""
now = datetime.now(timezone.utc)
c.execute("SELECT id, name, start_time, reminded FROM events")
rows = c.fetchall()
for event_id, name, start_time_str, reminded in rows:
start_time = datetime.fromisoformat(start_time_str)
# Send reminder if event starts within 24h and not yet reminded
if not reminded and now + timedelta(days=1) >= start_time:
for guild in bot.guilds:
event = discord.utils.get(guild.scheduled_events, id=int(event_id))
if event:
channel = bot.get_channel(DISCORD_CHANNEL_ID)
if channel:
await channel.send(f"⏰ Reminder: Event **{name}** starts tomorrow!")
# Telegram notification
await tg_bot.send_message(
chat_id=TELEGRAM_CHAT_ID,
text=f"⏰ Reminder: Event {name} starts tomorrow!",
)
c.execute("UPDATE events SET reminded=1 WHERE id=?", (event_id,))
conn.commit()
# =====================
# BASIC COMMANDS
# =====================
@bot.command()
async def hallo(ctx):
"""Simple hello command."""
await ctx.send(f"Hello {ctx.author.mention}! 👋")
# =====================
# PLAYLIST QUEUE
# =====================
playlist_queue = [] # Liste from Dicts: { 'url':..., 'title':..., 'data':... }
is_playing = False
async def play_next(ctx):
global is_playing
if playlist_queue:
next_track = playlist_queue.pop(0)
player = await YTDLSource.from_url(next_track['url'], loop=bot.loop, stream=True)
ctx.voice_client.play(
player,
after=lambda e: asyncio.run_coroutine_threadsafe(play_next(ctx), bot.loop)
)
await ctx.send(f"🎶 Now playing: **{player.title}**")
is_playing = True
else:
is_playing = False
await ctx.send("✅ Playlist finished.")
@bot.command(name="add")
async def add(ctx, *, url):
"""
Fügt einen Song oder eine komplette YouTube-Playlist zur Warteschlange hinzu.
"""
# Playlist oder Einzelvideo abrufen
data = await asyncio.get_event_loop().run_in_executor(
None, lambda: ytdl.extract_info(url, download=False)
)
added_titles = []
if 'entries' in data:
for entry in data['entries']:
playlist_queue.append({'url': entry['url'], 'title': entry['title'], 'data': entry})
added_titles.append(entry['title'])
else:
playlist_queue.append({'url': data['url'], 'title': data['title'], 'data': data})
added_titles.append(data['title'])
await ctx.send(f"✅ Added {len(added_titles)} track(s) to the playlist:\n" +
"\n".join(f"- {t}" for t in added_titles[:10]) +
(f"\n… +{len(added_titles)-10} more" if len(added_titles) > 10 else ""))
if not ctx.voice_client and ctx.author.voice:
await ctx.author.voice.channel.connect()
if ctx.voice_client and not ctx.voice_client.is_playing():
await play_next(ctx)
@bot.command(name="playlist")
async def playlist_cmd(ctx):
"""
Zeigt die aktuelle Playlist/Warteschlange an.
"""
if not playlist_queue:
await ctx.send("🎵 Die Playlist ist leer.")
else:
text = "\n".join([f"{idx+1}. {track['title']}" for idx, track in enumerate(playlist_queue[:20])])
await ctx.send(f"🎵 **Aktuelle Playlist:**\n{text}")
# =====================
# YTDL / FFMPEG SETUP
# =====================
ytdl_format_options = {
"format": "bestaudio/best",
"noplaylist": True,
"quiet": True,
"extract_flat": False,
"cookiefile": "cookies.txt"
}
ffmpeg_options = {
"before_options": "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5",
"options": "-vn -ar 48000 -ac 2 -b:a 192k", # Optimized audio for Discord
}
ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
class YTDLSource(discord.PCMVolumeTransformer):
def __init__(self, source, *, data, volume=0.5):
super().__init__(source, volume)
self.data = data
self.title = data.get("title")
@classmethod
async def from_url(cls, url, *, loop=None, stream=False):
loop = loop or asyncio.get_event_loop()
try:
data = await loop.run_in_executor(
None, lambda: ytdl.extract_info(url, download=not stream)
)
except Exception as e:
return None, e
if "entries" in data:
data = data["entries"][0]
filename = data["url"] if stream else ytdl.prepare_filename(data)
return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data), None
# =====================
# VOICE / MUSIC COMMANDS
# =====================
@bot.command(name="join")
async def join(ctx):
"""Join the voice channel the user is currently in."""
if ctx.author.voice:
channel = ctx.author.voice.channel
await channel.connect()
await ctx.send(f"Joined voice channel **{channel}**.")
else:
await ctx.send("You are not in a voice channel!")
@bot.command(name="leave", help="Bot leaves the voice channel.")
async def leave(ctx):
"""Disconnect from the current voice channel."""
if ctx.voice_client:
await ctx.voice_client.disconnect()
else:
await ctx.send("I am not in a voice channel!")
@bot.command(name="play")
async def play(ctx, *, url):
"""Streams Audio von einer YouTube URL"""
if not ctx.voice_client:
if ctx.author.voice:
await ctx.author.voice.channel.connect()
else:
await ctx.send("Du bist in keinem Sprachkanal!")
return
player, error = await YTDLSource.from_url(url, loop=bot.loop, stream=True)
if error or not player:
await ctx.send("⚠️ Konnte den Titel nicht abspielen. "
"YouTube Restrictions blockieren möglicherweise den Zugriff.")
return
ctx.voice_client.stop()
ctx.voice_client.play(player)
stop_button = Button(label="Stop", style=discord.ButtonStyle.red)
pause_button = Button(label="Pause", style=discord.ButtonStyle.gray)
resume_button = Button(label="Resume", style=discord.ButtonStyle.green)
async def stop_callback(interaction):
if ctx.voice_client:
ctx.voice_client.stop()
await interaction.response.edit_message(content="⏹️ Stopped", view=None)
async def pause_callback(interaction):
if ctx.voice_client and ctx.voice_client.is_playing():
ctx.voice_client.pause()
await interaction.response.defer()
async def resume_callback(interaction):
if ctx.voice_client and ctx.voice_client.is_paused():
ctx.voice_client.resume()
await interaction.response.defer()
stop_button.callback = stop_callback
pause_button.callback = pause_callback
resume_button.callback = resume_callback
view = View()
view.add_item(stop_button)
view.add_item(pause_button)
view.add_item(resume_button)
await ctx.send(f"🎶 Now playing: **{player.title}**", view=view)
@bot.command(name="stop", help="Stop the music.")
async def stop(ctx):
"""Stop the currently playing music."""
if ctx.voice_client and ctx.voice_client.is_playing():
ctx.voice_client.stop()
await ctx.send("⏹️ Playback stopped.")
else:
await ctx.send("Nothing is currently playing.")
# =====================
# BOT READY EVENT
# =====================
@bot.event
async def on_ready():
"""Called when the bot successfully connects to Discord."""
if not reminder_task.is_running():
reminder_task.start()
print(f"{bot.user} is online")
# =====================
# START BOT
# =====================
bot.run(BOT_TOKEN)