""" 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", "cookiefile": "/app/cookies.txt", "extractor_args": { "youtube": { "player_client": ["android", "web"], } }, "noplaylist": True, "quiet": True, } 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)