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