import random import re import discord import discord.opus import discord.voice_client import functools import sys import db import errors import youtube_dl import concurrent.futures import stagismo import platform import typing import os import asyncio import configparser import subprocess import async_timeout import raven # Queue emojis queue_emojis = [":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:", ":nine:", ":ten:"] # Init the event loop loop = asyncio.get_event_loop() # Init the config reader config = configparser.ConfigParser() config.read("config.ini") async def find_user(user: discord.User): session = await loop.run_in_executor(executor, db.Session) user = await loop.run_in_executor(executor, session.query(db.Discord).filter_by(discord_id=user.id).join(db.Royal).first) await loop.run_in_executor(executor, session.close) return user class DurationError(Exception): pass class Video: def __init__(self): self.enqueuer = None self.filename = None self.ytdl_url = None @staticmethod async def init(user_id: str, *, filename=None, ytdl_url=None): if filename is None and ytdl_url is None: raise Exception("Filename or url must be specified") self = Video() self.enqueuer = int(user_id) self.filename = filename self.ytdl_url = ytdl_url return self async def download(self): # Retrieve info before downloading with youtube_dl.YoutubeDL() as ytdl: info = await loop.run_in_executor(executor, functools.partial(ytdl.extract_info, self.ytdl_url, download=False)) if "entries" in info: info = info["entries"][0] file_id = info.get("title", str(hash(self.ytdl_url))) file_id = re.sub(r'[/\\?*"<>|]', "_", file_id) # Set the filename to the downloaded video self.filename = file_id if os.path.exists(f"opusfiles/{file_id}.opus"): return if info.get("duration", 1) > int(config["YouTube"]["max_duration"]): raise DurationError(f"File duration is over the limit " f"set in the config ({config['YouTube']['max_duration']}).") ytdl_args = {"noplaylist": True, "format": "best", "postprocessors": [{ "key": 'FFmpegExtractAudio', "preferredcodec": 'opus' }], "outtmpl": f"opusfiles/{file_id}.opus", "quiet": True} if "youtu" in self.ytdl_url: ytdl_args["username"] = config["YouTube"]["username"] ytdl_args["password"] = config["YouTube"]["password"] # Download the video with youtube_dl.YoutubeDL(ytdl_args) as ytdl: await loop.run_in_executor(executor, functools.partial(ytdl.download, [self.ytdl_url])) async def add_to_db(self): session = await loop.run_in_executor(executor, db.Session) pm = db.PlayedMusic(enqueuer_id=self.enqueuer, filename=self.filename) session.add(pm) await loop.run_in_executor(executor, session.commit) await loop.run_in_executor(executor, session.close) if __debug__: version = "Dev" else: # Find the latest git tag old_wd = os.getcwd() try: os.chdir(os.path.dirname(__file__)) version = str(subprocess.check_output(["git", "describe", "--tags"]), encoding="utf8").strip() except Exception: version = "❓" finally: os.chdir(old_wd) # Init the discord bot client = discord.Client() if platform.system() == "Linux": discord.opus.load_opus("/usr/lib/x86_64-linux-gnu/libopus.so") elif platform.system() == "Windows": discord.opus.load_opus("libopus-0.x64.dll") voice_client: typing.Optional[discord.VoiceClient] = None voice_player: typing.Optional[discord.voice_client.StreamPlayer] = None voice_queue: typing.List[Video] = [] # Init the executor executor = concurrent.futures.ThreadPoolExecutor(max_workers=3) # Init the Sentry client sentry = raven.Client(config["Sentry"]["token"], release=version) async def on_error(event, *args, **kwargs): ei = sys.exc_info() print("ERRORE CRITICO:\n" + repr(ei[1]) + "\n\n" + repr(ei)) try: await client.send_message(client.get_channel(config["Discord"]["main_channel"]), f"☢️ **ERRORE CRITICO NELL'EVENTO** `{event}`\n" f"Il bot si è chiuso e si dovrebbe riavviare entro qualche minuto.\n" f"Una segnalazione di errore è stata automaticamente mandata a Steffo.\n\n" f"Dettagli dell'errore:\n" f"```python\n" f"{repr(ei[1])}\n" f"```") if voice_client is not None: await voice_client.disconnect() await client.change_presence(status=discord.Status.invisible) await client.close() except Exception as e: print("ERRORE CRITICO PIU' CRITICO:\n" + repr(e) + "\n\n" + repr(sys.exc_info())) loop.stop() sentry.captureException(exc_info=ei) os._exit(1) pass @client.event async def on_ready(): await client.send_message(client.get_channel(config["Discord"]["main_channel"]), f"ℹ Royal Bot {version} avviato e pronto a ricevere comandi!") await client.change_presence(game=None, status=discord.Status.online) @client.event async def on_message(message: discord.Message): global voice_queue global voice_player if not message.content.startswith("!"): return sentry.user_context({ "discord": { "discord_id": message.author.id, "name": message.author.name, "discriminator": message.author.discriminator } }) await client.send_typing(message.channel) session = await loop.run_in_executor(executor, db.Session) user = session.query(db.Discord).filter_by(discord_id=message.author.id).one_or_none() if user is None: user = db.Discord(discord_id=message.author.id, name=message.author.name, discriminator=message.author.discriminator, avatar_hex=message.author.avatar) session.add(user) await loop.run_in_executor(executor, session.commit) else: sentry.user_context({ "discord": { "discord_id": message.author.id, "name": message.author.name, "discriminator": message.author.discriminator }, "royal": { "user_id": user.royal_id } }) if message.content.startswith("!link"): if user.royal_id is None: await client.send_message(message.channel, "⚠️ Il tuo account Discord è già collegato a un account RYG " "o l'account RYG che hai specificato è già collegato a un account Discord.") return try: username = message.content.split(" ", 1)[1] except IndexError: await client.send_message(message.channel, "⚠️ Non hai specificato un username!\n" "Sintassi corretta: `!link `") return royal = session.query(db.Royal).filter_by(username=username).one_or_none() if royal is None: await client.send_message(message.channel, "⚠️ Non esiste nessun account RYG con questo nome.") return user.royal_id = royal.id session.commit() session.close() await client.send_message(message.channel, "✅ Sincronizzazione completata!") elif message.content.startswith("!cv"): await client.send_typing(message.channel) if message.author.voice.voice_channel is None: await client.send_message(message.channel, "⚠ Non sei in nessun canale!") return global voice_client if voice_client is not None and voice_client.is_connected(): await voice_client.move_to(message.author.voice.voice_channel) else: voice_client = await client.join_voice_channel(message.author.voice.voice_channel) await client.send_message(message.channel, f"✅ Mi sono connesso in <#{message.author.voice.voice_channel.id}>.") elif message.content.startswith("!play"): await client.send_typing(message.channel) # The bot should be in voice chat if voice_client is None: await client.send_message(message.channel, "⚠️ Non sono connesso alla cv!\n" "Fammi entrare scrivendo `!cv` mentre sei in chat vocale.") return # Find the sent url try: url = message.content.split(" ", 1)[1] except IndexError: await client.send_message(message.channel, "⚠️ Non hai specificato un url!\n" "Sintassi corretta: `!play