First Init
This commit is contained in:
commit
1667827dc4
4 changed files with 362 additions and 0 deletions
4
.env-example
Normal file
4
.env-example
Normal file
|
@ -0,0 +1,4 @@
|
|||
BOT_TOKEN=dein_discord_token
|
||||
DISCORD_CHANNEL_ID=123456789012345678
|
||||
TELEGRAM_TOKEN=dein_telegram_token
|
||||
TELEGRAM_CHAT_ID=-1001234567890
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
.venv/
|
||||
.env
|
||||
events.db
|
72
requirements.txt
Normal file
72
requirements.txt
Normal file
|
@ -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
|
283
skullbot.py
Normal file
283
skullbot.py
Normal file
|
@ -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)
|
Loading…
Add table
Reference in a new issue