From 1667827dc4c29a07c49921efc6a7435b1d9357df Mon Sep 17 00:00:00 2001 From: = <=> Date: Fri, 19 Sep 2025 22:58:50 +0200 Subject: [PATCH] First Init --- .env-example | 4 + .gitignore | 3 + requirements.txt | 72 ++++++++++++ skullbot.py | 283 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 362 insertions(+) create mode 100644 .env-example create mode 100644 .gitignore create mode 100644 requirements.txt create mode 100644 skullbot.py diff --git a/.env-example b/.env-example new file mode 100644 index 0000000..8200925 --- /dev/null +++ b/.env-example @@ -0,0 +1,4 @@ +BOT_TOKEN=dein_discord_token +DISCORD_CHANNEL_ID=123456789012345678 +TELEGRAM_TOKEN=dein_telegram_token +TELEGRAM_CHAT_ID=-1001234567890 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..200d5fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.venv/ +.env +events.db \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e77a3ef --- /dev/null +++ b/requirements.txt @@ -0,0 +1,72 @@ +annotated-types==0.7.0 +ansible==11.7.0 +ansible-core==2.18.6 +anyio==3.6.2 +apache-libcloud==3.4.1 +argcomplete==2.0.0 +attrs==25.3.0 +Babel==2.10.3 +certifi==2022.9.24 +chardet==5.1.0 +charset-normalizer==3.0.1 +click==8.1.3 +cloudflare==4.3.1 +colorama==0.4.6 +cryptography==38.0.4 +distro==1.9.0 +dnspython==2.3.0 +docopt==0.6.2 +fysom==2.1.6 +h11==0.14.0 +h2==4.1.0 +hpack==4.0.0 +httpcore==0.16.3 +httplib2==0.20.4 +httpx==0.23.3 +hyperframe==6.0.0 +idna==3.3 +Jinja2==3.1.2 +jmespath==1.0.1 +JSON-minify==0.3.0 +jsonschema==4.24.0 +jsonschema-specifications==2025.4.1 +libvirt-python==9.0.0 +lockfile==0.12.2 +markdown-it-py==2.1.0 +MarkupSafe==2.1.2 +mdurl==0.1.2 +netaddr==0.8.0 +ntlm-auth==1.4.0 +packaging==23.0 +passlib==1.7.4 +pycairo==1.20.1 +pydantic==2.11.7 +pydantic_core==2.33.2 +Pygments==2.14.0 +PyGObject==3.42.2 +pykerberos==1.1.14 +pyparsing==3.0.9 +python-apt==2.6.0 +python-dateutil==2.9.0.post0 +python-dotenv==1.1.1 +pytz==2022.7.1 +pywinrm==0.3.0 +PyYAML==6.0 +referencing==0.36.2 +requests==2.28.1 +requests-kerberos==0.12.0 +requests-ntlm==1.1.0 +requests-toolbelt==0.10.1 +resolvelib==0.9.0 +rfc3986==1.5.0 +rich==13.3.1 +rpds-py==0.25.1 +selinux==3.4 +simplejson==3.18.3 +six==1.16.0 +sniffio==1.2.0 +termcolor==3.1.0 +typing-inspection==0.4.1 +typing_extensions==4.14.0 +urllib3==1.26.12 +xmltodict==0.13.0 diff --git a/skullbot.py b/skullbot.py new file mode 100644 index 0000000..1e62812 --- /dev/null +++ b/skullbot.py @@ -0,0 +1,283 @@ +""" +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 = int(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}! 👋") + +# ===================== +# 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): + """ + Handles downloading and streaming audio from YouTube. + """ + + 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() + data = await loop.run_in_executor( + None, lambda: ytdl.extract_info(url, download=not stream) + ) + 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) + +# ===================== +# 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 from a given YouTube URL to the connected voice channel. + Adds interactive buttons to control playback. + """ + # Connect to voice channel if not already connected + if not ctx.voice_client: + if ctx.author.voice: + await ctx.author.voice.channel.connect() + else: + await ctx.send("You are not in a voice channel!") + return + + # Start streaming + player = await YTDLSource.from_url(url, loop=bot.loop, stream=True) + ctx.voice_client.stop() + ctx.voice_client.play(player) + + # Create interactive buttons + 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() + + # Attach callbacks + stop_button.callback = stop_callback + pause_button.callback = pause_callback + resume_button.callback = resume_callback + + # Build and send view + 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)