2017-11-11 17:55:13 +00:00
import random
2017-10-27 11:38:32 +00:00
import discord
2017-10-31 22:50:05 +00:00
import discord . opus
2018-02-25 18:50:57 +00:00
import discord . voice_client
2017-11-01 18:23:11 +00:00
import functools
2017-11-09 19:34:18 +00:00
import sys
2017-10-27 11:38:32 +00:00
import db
import errors
2017-11-05 21:50:37 +00:00
import youtube_dl
2018-01-15 12:42:40 +00:00
import concurrent . futures
2018-01-25 14:24:17 +00:00
import stagismo
2018-02-25 18:50:57 +00:00
import platform
import typing
2018-02-25 19:47:12 +00:00
import os
2017-10-27 11:38:32 +00:00
# Init the event loop
import asyncio
loop = asyncio . get_event_loop ( )
# Init the config reader
import configparser
config = configparser . ConfigParser ( )
config . read ( " config.ini " )
2018-02-25 19:47:12 +00:00
class DurationError ( Exception ) :
pass
2017-11-06 15:54:36 +00:00
class Video :
def __init__ ( self ) :
self . user = None
2018-02-25 18:50:57 +00:00
self . filename = None
self . ytdl_url = None
2017-11-01 18:23:11 +00:00
2017-11-06 15:54:36 +00:00
@staticmethod
2018-02-25 18:50:57 +00:00
async def init ( user , filename = None , ytdl_url = None ) :
if filename is None and ytdl_url is None :
raise Exception ( " Filename or url must be specified " )
2017-11-06 15:54:36 +00:00
self = Video ( )
2018-02-25 18:50:57 +00:00
discord_user = await find_user ( user )
2017-11-07 17:44:00 +00:00
self . user = discord_user . royal if discord_user is not None else None
2018-02-25 18:50:57 +00:00
self . filename = filename
self . ytdl_url = ytdl_url
2017-11-06 15:54:36 +00:00
return self
2018-01-25 14:30:07 +00:00
2018-02-25 19:47:12 +00:00
async def download ( self ) :
# Retrieve info before downloading
try :
with youtube_dl . YoutubeDL ( ) as ytdl :
info = await loop . run_in_executor ( executor , functools . partial ( ytdl . extract_info , self . ytdl_url , download = False ) )
file_id = info [ " entries " ] [ 0 ] . get ( " title " , hash ( self . ytdl_url ) )
except Exception as e :
print ( e )
raise e
if os . path . exists ( f " opusfiles/ { file_id } .opus " ) :
return
if info [ " entries " ] [ 0 ] [ " duration " ] > int ( config [ " YouTube " ] [ " max_duration " ] ) :
raise DurationError ( f " File duration is over the limit set in the config ( { config [ ' YouTube ' ] [ ' max_duration ' ] } ). " )
2018-02-25 18:50:57 +00:00
ytdl_args = { " noplaylist " : True ,
2018-02-25 19:47:12 +00:00
" format " : " best " ,
2018-02-25 18:50:57 +00:00
" postprocessors " : [ {
" key " : ' FFmpegExtractAudio ' ,
" preferredcodec " : ' opus '
} ] ,
2018-02-25 19:47:12 +00:00
" outtmpl " : f " opusfiles/ { file_id } .opus " ,
" quiet " : True }
2018-02-25 18:50:57 +00:00
if " youtu " in self . ytdl_url :
ytdl_args [ " username " ] = config [ " YouTube " ] [ " username " ]
ytdl_args [ " password " ] = config [ " YouTube " ] [ " password " ]
# Download the video
2017-11-07 17:44:00 +00:00
try :
2018-02-25 18:50:57 +00:00
with youtube_dl . YoutubeDL ( ytdl_args ) as ytdl :
await loop . run_in_executor ( executor , functools . partial ( ytdl . download , [ self . ytdl_url ] ) )
2017-11-07 17:44:00 +00:00
except Exception as e :
2018-02-25 18:50:57 +00:00
print ( e )
2018-02-25 19:47:12 +00:00
raise e
2018-02-25 18:50:57 +00:00
# Set the filename to the downloaded video
self . filename = f " opusfiles/ { file_id } .opus "
if __debug__ :
version = " Dev "
else :
# Find the latest git tag
import subprocess
old_wd = os . getcwd ( )
try :
os . chdir ( os . path . dirname ( __file__ ) )
version = str ( subprocess . check_output ( [ " git " , " describe " , " --tags " ] ) , encoding = " utf8 " ) . strip ( )
except :
version = " v??? "
finally :
os . chdir ( old_wd )
2017-11-06 15:54:36 +00:00
2018-02-25 18:50:57 +00:00
# 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.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 )
2017-11-06 15:54:36 +00:00
async def find_user ( user : discord . User ) :
2018-01-15 12:42:40 +00:00
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 )
2017-11-06 15:54:36 +00:00
return user
2017-11-05 21:50:37 +00:00
2017-11-10 07:53:48 +00:00
async def on_error ( event , * args , * * kwargs ) :
type , exception , traceback = sys . exc_info ( )
try :
2017-11-20 09:49:58 +00:00
await client . send_message ( client . get_channel ( config [ " Discord " ] [ " main_channel " ] ) , f " ☢️ ERRORE CRITICO NELL ' EVENTO ` { event } ` \n "
2017-11-15 09:48:58 +00:00
f " Il bot si è chiuso e si dovrebbe riavviare entro qualche minuto. \n \n "
2017-11-10 07:53:48 +00:00
f " Dettagli dell ' errore: \n "
f " ```python \n "
f " { repr ( exception ) } \n "
f " ``` " )
2017-11-11 17:55:13 +00:00
await voice_client . disconnect ( )
2017-11-10 07:53:48 +00:00
await client . change_presence ( status = discord . Status . invisible )
await client . close ( )
except Exception as e :
print ( " ERRORE CRITICO PIU ' CRITICO: \n " + repr ( e ) + " \n " + repr ( sys . exc_info ( ) ) )
loop . stop ( )
2017-11-15 09:48:58 +00:00
os . _exit ( 1 )
pass
2017-11-09 19:34:18 +00:00
2017-11-10 07:53:48 +00:00
@client.event
async def on_ready ( ) :
2017-11-11 17:55:13 +00:00
await client . send_message ( client . get_channel ( " 368447084518572034 " ) , f " ℹ Royal Bot { version } avviato e pronto a ricevere comandi! " )
2017-11-10 07:53:48 +00:00
await client . change_presence ( game = None , status = discord . Status . online )
2017-10-27 11:38:32 +00:00
@client.event
async def on_message ( message : discord . Message ) :
2017-11-07 17:44:00 +00:00
global voice_queue
global voice_player
2017-11-01 18:23:11 +00:00
if message . content . startswith ( " !register " ) :
await client . send_typing ( message . channel )
2018-01-15 12:42:40 +00:00
session = await loop . run_in_executor ( executor , db . Session ( ) )
2017-11-01 18:23:11 +00:00
try :
username = message . content . split ( " " , 1 ) [ 1 ]
except IndexError :
2017-11-05 21:50:37 +00:00
await client . send_message ( message . channel , " ⚠️ Non hai specificato un username! \n "
" Sintassi corretta: `!register <username_ryg>` " )
2017-11-01 18:23:11 +00:00
return
try :
d = db . Discord . create ( session ,
royal_username = username ,
discord_user = message . author )
except errors . AlreadyExistingError :
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
session . add ( d )
session . commit ( )
2017-10-30 12:45:38 +00:00
session . close ( )
2017-11-01 18:23:11 +00:00
await client . send_message ( message . channel , " ✅ Sincronizzazione completata! " )
2017-11-11 17:55:13 +00:00
elif message . content . startswith ( " !cv " ) :
2017-11-01 18:23:11 +00:00
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
2017-11-07 17:44:00 +00:00
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 )
2017-11-01 18:23:11 +00:00
await client . send_message ( message . channel , f " ✅ Mi sono connesso in <# { message . author . voice . voice_channel . id } >. " )
2017-11-09 19:34:18 +00:00
elif message . content . startswith ( " !play " ) :
2017-11-01 18:23:11 +00:00
await client . send_typing ( message . channel )
2017-11-05 21:50:37 +00:00
# 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. " )
2017-11-09 19:34:18 +00:00
return
2017-11-05 21:50:37 +00:00
# Find the sent url
2017-11-01 18:23:11 +00:00
try :
url = message . content . split ( " " , 1 ) [ 1 ]
except IndexError :
2017-11-05 21:50:37 +00:00
await client . send_message ( message . channel , " ⚠️ Non hai specificato un url! \n "
2017-11-11 17:55:13 +00:00
" Sintassi corretta: `!play <video>` " )
2017-11-01 18:23:11 +00:00
return
2017-11-05 21:50:37 +00:00
# Se è una playlist, informa che potrebbe essere richiesto un po' di tempo
if " playlist " in url :
await client . send_message ( message . channel , f " ℹ ️ Hai inviato una playlist al bot.\n "
f " L ' elaborazione potrebbe richiedere un po ' di tempo. " )
2018-02-25 18:50:57 +00:00
# If target is a single video
video = await Video . init ( user = message . author , ytdl_url = url )
await client . send_message ( message . channel , f " ✅ Aggiunto alla coda: ` { url } ` " )
voice_queue . append ( video )
2017-11-09 19:34:18 +00:00
elif message . content . startswith ( " !search " ) :
2017-11-07 17:44:00 +00:00
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. " )
2017-11-09 19:34:18 +00:00
return
2017-11-07 17:44:00 +00:00
# Find the sent text
try :
text = message . content . split ( " " , 1 ) [ 1 ]
except IndexError :
await client . send_message ( message . channel , " ⚠️ Non hai specificato il titolo! \n "
2017-11-11 17:55:13 +00:00
" Sintassi corretta: `!search <titolo>` " )
2017-11-07 17:44:00 +00:00
return
2018-02-25 18:50:57 +00:00
# If target is a single video
video = await Video . init ( user = message . author , ytdl_url = f " ytsearch: { text } " )
await client . send_message ( message . channel , f " ✅ Aggiunto alla coda: `ytsearch: { text } ` " )
voice_queue . append ( video )
elif message . content . startswith ( " !file " ) :
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 text
2017-11-07 17:44:00 +00:00
try :
2018-02-25 18:50:57 +00:00
text : str = message . content . split ( " " , 1 ) [ 1 ]
except IndexError :
await client . send_message ( message . channel , " ⚠️ Non hai specificato il nome del file! \n "
" Sintassi corretta: `!file <nomefile>` " )
return
# Ensure the filename ends with .opus
if not text . endswith ( " .opus " ) :
await client . send_message ( message . channel , " ⚠️ Il nome file specificato non è valido. " )
2017-11-07 17:44:00 +00:00
return
# If target is a single video
2018-02-25 18:50:57 +00:00
video = await Video . init ( user = message . author , filename = text )
await client . send_message ( message . channel , f " ✅ Aggiunto alla coda: ` { text } ` " )
2017-11-07 17:44:00 +00:00
voice_queue . append ( video )
2017-11-09 19:34:18 +00:00
elif message . content . startswith ( " !skip " ) :
2017-11-07 17:44:00 +00:00
global voice_player
voice_player . stop ( )
await client . send_message ( message . channel , f " ⏩ Video saltato. " )
2017-11-09 19:34:18 +00:00
elif message . content . startswith ( " !pause " ) :
2017-11-07 17:44:00 +00:00
if voice_player is None or not voice_player . is_playing ( ) :
await client . send_message ( message . channel , f " ⚠️ Non è in corso la riproduzione di un video, pertanto non c ' è niente da pausare. " )
return
voice_player . pause ( )
await client . send_message ( message . channel , f " ⏸ Riproduzione messa in pausa. \n "
2017-11-11 17:55:13 +00:00
f " Riprendi con `!resume`. " )
2017-11-09 19:34:18 +00:00
elif message . content . startswith ( " !resume " ) :
2017-11-07 17:44:00 +00:00
if voice_player is None or voice_player . is_playing ( ) :
await client . send_message ( message . channel , f " ⚠️ Non c ' è nulla in pausa da riprendere! " )
return
voice_player . resume ( )
await client . send_message ( message . channel , f " ▶️ Riproduzione ripresa. " )
2017-11-09 19:34:18 +00:00
elif message . content . startswith ( " !cancel " ) :
2018-02-25 18:50:57 +00:00
if not len ( voice_queue ) > 1 :
await client . send_message ( message . channel , f " ⚠ Non ci sono video da annullare. " )
2017-11-07 17:44:00 +00:00
return
2018-02-25 18:50:57 +00:00
video = voice_queue . pop ( )
await client . send_message ( message . channel , f " ❌ L ' ultimo video aggiunto alla playlist è stato rimosso. " )
2017-11-09 19:34:18 +00:00
elif message . content . startswith ( " !stop " ) :
2017-11-07 17:44:00 +00:00
if voice_player is None :
await client . send_message ( message . channel , f " ⚠ Non c ' è nulla da interrompere! " )
return
voice_queue = [ ]
voice_player . stop ( )
voice_player = None
await client . send_message ( message . channel , f " ⏹ Riproduzione interrotta e playlist svuotata. " )
2018-02-25 18:50:57 +00:00
#elif message.content.startswith("!np"):
# if voice_player is None or not voice_player.is_playing():
# await client.send_message(message.channel, f"ℹ Non c'è nulla in riproduzione al momento.")
# return
# await client.send_message(message.channel, f"▶️ Ora in riproduzione in <#{voice_client.channel.id}>:", embed=voice_playing.create_embed())
#elif message.content.startswith("!queue"):
# if voice_player is None:
# await client.send_message(message.channel, f"ℹ Non c'è nulla in riproduzione al momento.")
# return
# to_send = ""
# to_send += f"0. {voice_playing.info['title'] if voice_playing.info['title'] is not None else '_Senza titolo_'} - <{voice_playing.info['webpage_url'] if voice_playing.info['webpage_url'] is not None else ''}>\n"
# for n, video in enumerate(voice_queue):
# to_send += f"{n+1}. {video.info['title'] if video.info['title'] is not None else '_Senza titolo_'} - <{video.info['webpage_url'] if video.info['webpage_url'] is not None else ''}>\n"
# if len(to_send) >= 2000:
# to_send = to_send[0:1997] + "..."
# break
# await client.send_message(message.channel, to_send)
2017-11-11 17:55:13 +00:00
elif message . content . startswith ( " !cast " ) :
try :
spell = message . content . split ( " " , 1 ) [ 1 ]
except IndexError :
await client . send_message ( " ⚠️ Non hai specificato nessun incantesimo! \n "
" Sintassi corretta: `!cast <nome_incantesimo>` " )
return
target = random . sample ( list ( message . server . members ) , 1 ) [ 0 ]
# Seed the rng with the spell name
# so that spells always deal the same damage
random . seed ( spell )
dmg_mod = random . randrange ( - 2 , 3 )
dmg_dice = random . randrange ( 1 , 4 )
dmg_max = random . sample ( [ 4 , 6 , 8 , 10 , 12 , 20 , 100 ] , 1 ) [ 0 ]
# Reseed the rng with a random value
# so that the dice roll always deals a different damage
random . seed ( )
total = dmg_mod
for dice in range ( 0 , dmg_dice ) :
total + = random . randrange ( 1 , dmg_max + 1 )
await client . send_message ( message . channel , f " ❇️ Ho lanciato ** { spell } ** su ** { target . nick if target . nick is not None else target . name } ** per { dmg_dice } d { dmg_max } { ' + ' if dmg_mod > 0 else ' ' } { str ( dmg_mod ) if dmg_mod != 0 else ' ' } =** { total if total > 0 else 0 } ** danni! " )
2018-01-25 14:24:17 +00:00
elif message . content . startswith ( " !smecds " ) :
ds = random . sample ( stagismo . listona , 1 ) [ 0 ]
await client . send_message ( message . channel , f " Secondo me, è colpa { ds } . " , tts = True )
2017-11-09 19:34:18 +00:00
elif __debug__ and message . content . startswith ( " !exception " ) :
2017-11-10 07:53:48 +00:00
raise Exception ( " !exception was called " )
2017-11-05 21:50:37 +00:00
2017-10-30 12:45:38 +00:00
async def update_users_pipe ( users_connection ) :
await client . wait_until_ready ( )
while True :
2018-01-15 12:42:40 +00:00
msg = await loop . run_in_executor ( executor , users_connection . recv )
2017-10-30 12:45:38 +00:00
if msg == " /cv " :
discord_members = list ( client . get_server ( config [ " Discord " ] [ " server_id " ] ) . members )
users_connection . send ( discord_members )
2017-11-05 21:50:37 +00:00
async def update_music_queue ( ) :
2017-11-06 15:54:36 +00:00
await client . wait_until_ready ( )
2018-02-25 18:50:57 +00:00
global voice_client
2017-11-20 09:49:58 +00:00
global voice_player
2018-02-25 18:50:57 +00:00
global voice_queue
2017-11-06 15:54:36 +00:00
while True :
2018-02-25 18:50:57 +00:00
if voice_client is None :
await asyncio . sleep ( 5 )
continue
if voice_player is not None and not voice_player . _end . is_set ( ) :
2017-11-07 17:44:00 +00:00
await asyncio . sleep ( 1 )
continue
if len ( voice_queue ) == 0 :
2018-02-25 18:50:57 +00:00
await client . change_presence ( )
2017-11-07 17:44:00 +00:00
await asyncio . sleep ( 1 )
continue
2018-02-25 18:50:57 +00:00
video = voice_queue . pop ( )
if video . ytdl_url :
await client . send_message ( client . get_channel ( config [ " Discord " ] [ " main_channel " ] ) , f " ℹ E' iniziato il download di ` { video . ytdl_url } `. " )
2018-02-25 19:47:12 +00:00
try :
await video . download ( )
except DurationError :
await client . send_message ( client . get_channel ( config [ " Discord " ] [ " main_channel " ] ) , f " ⚠ Il file supera il limite di durata impostato in config.ini (` { config [ ' YouTube ' ] [ ' max_duration ' ] } ` secondi). " )
continue
except Exception as e :
await client . send_message ( client . get_channel ( config [ " Discord " ] [ " main_channel " ] ) , f " ⚠️ C ' è stato un errore durante il download di ` { video . ytdl_url } `: \n "
f " ``` \n "
f " { e } \n "
f " ``` " )
2018-02-25 18:50:57 +00:00
continue
voice_player = voice_client . create_ffmpeg_player ( video . filename )
2017-11-06 15:54:36 +00:00
voice_player . start ( )
2018-02-25 18:50:57 +00:00
await client . send_message ( client . get_channel ( config [ " Discord " ] [ " main_channel " ] ) , f " ▶ Ora in riproduzione in <# { voice_client . channel . id } >: \n "
f " ` { video . filename } ` " )
2018-02-25 19:47:12 +00:00
await client . change_presence ( game = discord . Game ( name = " youtube-dl " , type = 2 ) )
2017-11-06 15:54:36 +00:00
2017-11-05 21:50:37 +00:00
2018-01-15 12:42:40 +00:00
def process ( users_connection = None ) :
2017-10-30 12:45:38 +00:00
print ( " Discordbot starting... " )
2018-01-15 12:42:40 +00:00
if users_connection is not None :
asyncio . ensure_future ( update_users_pipe ( users_connection ) )
asyncio . ensure_future ( update_music_queue ( ) )
2017-11-09 19:34:18 +00:00
client . on_error = on_error
2018-01-15 12:42:40 +00:00
client . run ( config [ " Discord " ] [ " bot_token " ] )
if __name__ == " __main__ " :
2018-01-25 14:30:07 +00:00
process ( )