344 lines
11 KiB
Python
344 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,
|
|
}
|
|
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)
|