Advertisement
mmb0b123

Untitled

Oct 19th, 2018
102
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 124.06 KB | None | 0 0
  1. import os
  2. import sys
  3. import time
  4. import shlex
  5. import shutil
  6. import random
  7. import inspect
  8. import logging
  9. import asyncio
  10. import pathlib
  11. import traceback
  12. import math
  13. import re
  14.  
  15. import aiohttp
  16. import discord
  17. import colorlog
  18.  
  19. from io import BytesIO, StringIO
  20. from functools import wraps
  21. from textwrap import dedent
  22. from datetime import timedelta
  23. from collections import defaultdict
  24.  
  25. from discord.enums import ChannelType
  26.  
  27. from . import exceptions
  28. from . import downloader
  29.  
  30. from .playlist import Playlist
  31. from .player import MusicPlayer
  32. from .entry import StreamPlaylistEntry
  33. from .opus_loader import load_opus_lib
  34. from .config import Config, ConfigDefaults
  35. from .permissions import Permissions, PermissionsDefaults
  36. from .constructs import SkipState, Response
  37. from .utils import load_file, write_file, fixg, ftimedelta, _func_, _get_variable
  38. from .spotify import Spotify
  39. from .json import Json
  40.  
  41. from .constants import VERSION as BOTVERSION
  42. from .constants import DISCORD_MSG_CHAR_LIMIT, AUDIO_CACHE_PATH
  43.  
  44.  
  45. load_opus_lib()
  46.  
  47. log = logging.getLogger(__name__)
  48.  
  49.  
  50. class MusicBot(discord.Client):
  51. def __init__(self, config_file=None, perms_file=None):
  52. try:
  53. sys.stdout.write("\x1b]2;MusicBot {}\x07".format(BOTVERSION))
  54. except:
  55. pass
  56.  
  57. print()
  58.  
  59. if config_file is None:
  60. config_file = ConfigDefaults.options_file
  61.  
  62. if perms_file is None:
  63. perms_file = PermissionsDefaults.perms_file
  64.  
  65. self.players = {}
  66. self.exit_signal = None
  67. self.init_ok = False
  68. self.cached_app_info = None
  69. self.last_status = None
  70.  
  71. self.config = Config(config_file)
  72. self.permissions = Permissions(perms_file, grant_all=[self.config.owner_id])
  73. self.str = Json(self.config.i18n_file)
  74.  
  75. self.blacklist = set(load_file(self.config.blacklist_file))
  76. self.autoplaylist = load_file(self.config.auto_playlist_file)
  77.  
  78. self.aiolocks = defaultdict(asyncio.Lock)
  79. self.downloader = downloader.Downloader(download_folder='audio_cache')
  80.  
  81. self._setup_logging()
  82.  
  83. log.info('Starting MusicBot {}'.format(BOTVERSION))
  84.  
  85. if not self.autoplaylist:
  86. log.warning("Autoplaylist is empty, disabling.")
  87. self.config.auto_playlist = False
  88. else:
  89. log.info("Loaded autoplaylist with {} entries".format(len(self.autoplaylist)))
  90.  
  91. if self.blacklist:
  92. log.debug("Loaded blacklist with {} entries".format(len(self.blacklist)))
  93.  
  94. # TODO: Do these properly
  95. ssd_defaults = {
  96. 'last_np_msg': None,
  97. 'auto_paused': False,
  98. 'availability_paused': False
  99. }
  100. self.server_specific_data = defaultdict(ssd_defaults.copy)
  101.  
  102. super().__init__()
  103. self.aiosession = aiohttp.ClientSession(loop=self.loop)
  104. self.http.user_agent += ' MusicBot/%s' % BOTVERSION
  105.  
  106. self.spotify = None
  107. if self.config._spotify:
  108. try:
  109. self.spotify = Spotify(self.config.spotify_clientid, self.config.spotify_clientsecret, aiosession=self.aiosession, loop=self.loop)
  110. if not self.spotify.token:
  111. log.warning('Spotify did not provide us with a token. Disabling.')
  112. self.config._spotify = False
  113. else:
  114. log.info('Authenticated with Spotify successfully using client ID and secret.')
  115. except exceptions.SpotifyError as e:
  116. log.warning('There was a problem initialising the connection to Spotify. Is your client ID and secret correct? Details: {0}. Continuing anyway in 5 seconds...'.format(e))
  117. self.config._spotify = False
  118. time.sleep(5) # make sure they see the problem
  119.  
  120. def __del__(self):
  121. # These functions return futures but it doesn't matter
  122. try: self.http.session.close()
  123. except: pass
  124.  
  125. # TODO: Add some sort of `denied` argument for a message to send when someone else tries to use it
  126. def owner_only(func):
  127. @wraps(func)
  128. async def wrapper(self, *args, **kwargs):
  129. # Only allow the owner to use these commands
  130. orig_msg = _get_variable('message')
  131.  
  132. if not orig_msg or orig_msg.author.id == self.config.owner_id:
  133. # noinspection PyCallingNonCallable
  134. return await func(self, *args, **kwargs)
  135. else:
  136. raise exceptions.PermissionsError("Only the owner can use this command.", expire_in=30)
  137.  
  138. return wrapper
  139.  
  140. def dev_only(func):
  141. @wraps(func)
  142. async def wrapper(self, *args, **kwargs):
  143. orig_msg = _get_variable('message')
  144.  
  145. if str(orig_msg.author.id) in self.config.dev_ids:
  146. # noinspection PyCallingNonCallable
  147. return await func(self, *args, **kwargs)
  148. else:
  149. raise exceptions.PermissionsError("Only dev users can use this command.", expire_in=30)
  150.  
  151. wrapper.dev_cmd = True
  152. return wrapper
  153.  
  154. def ensure_appinfo(func):
  155. @wraps(func)
  156. async def wrapper(self, *args, **kwargs):
  157. await self._cache_app_info()
  158. # noinspection PyCallingNonCallable
  159. return await func(self, *args, **kwargs)
  160.  
  161. return wrapper
  162.  
  163. def _get_owner(self, *, server=None, voice=False):
  164. return discord.utils.find(
  165. lambda m: m.id == self.config.owner_id and (m.voice if voice else True),
  166. server.members if server else self.get_all_members()
  167. )
  168.  
  169. def _delete_old_audiocache(self, path=AUDIO_CACHE_PATH):
  170. try:
  171. shutil.rmtree(path)
  172. return True
  173. except:
  174. try:
  175. os.rename(path, path + '__')
  176. except:
  177. return False
  178. try:
  179. shutil.rmtree(path)
  180. except:
  181. os.rename(path + '__', path)
  182. return False
  183.  
  184. return True
  185.  
  186. def _setup_logging(self):
  187. if len(logging.getLogger(__package__).handlers) > 1:
  188. log.debug("Skipping logger setup, already set up")
  189. return
  190.  
  191. shandler = logging.StreamHandler(stream=sys.stdout)
  192. shandler.setFormatter(colorlog.LevelFormatter(
  193. fmt = {
  194. 'DEBUG': '{log_color}[{levelname}:{module}] {message}',
  195. 'INFO': '{log_color}{message}',
  196. 'WARNING': '{log_color}{levelname}: {message}',
  197. 'ERROR': '{log_color}[{levelname}:{module}] {message}',
  198. 'CRITICAL': '{log_color}[{levelname}:{module}] {message}',
  199.  
  200. 'EVERYTHING': '{log_color}[{levelname}:{module}] {message}',
  201. 'NOISY': '{log_color}[{levelname}:{module}] {message}',
  202. 'VOICEDEBUG': '{log_color}[{levelname}:{module}][{relativeCreated:.9f}] {message}',
  203. 'FFMPEG': '{log_color}[{levelname}:{module}][{relativeCreated:.9f}] {message}'
  204. },
  205. log_colors = {
  206. 'DEBUG': 'cyan',
  207. 'INFO': 'white',
  208. 'WARNING': 'yellow',
  209. 'ERROR': 'red',
  210. 'CRITICAL': 'bold_red',
  211.  
  212. 'EVERYTHING': 'white',
  213. 'NOISY': 'white',
  214. 'FFMPEG': 'bold_purple',
  215. 'VOICEDEBUG': 'purple',
  216. },
  217. style = '{',
  218. datefmt = ''
  219. ))
  220. shandler.setLevel(self.config.debug_level)
  221. logging.getLogger(__package__).addHandler(shandler)
  222.  
  223. log.debug("Set logging level to {}".format(self.config.debug_level_str))
  224.  
  225. if self.config.debug_mode:
  226. dlogger = logging.getLogger('discord')
  227. dlogger.setLevel(logging.DEBUG)
  228. dhandler = logging.FileHandler(filename='logs/discord.log', encoding='utf-8', mode='w')
  229. dhandler.setFormatter(logging.Formatter('{asctime}:{levelname}:{name}: {message}', style='{'))
  230. dlogger.addHandler(dhandler)
  231.  
  232. @staticmethod
  233. def _check_if_empty(vchannel: discord.abc.GuildChannel, *, excluding_me=True, excluding_deaf=False):
  234. def check(member):
  235. if excluding_me and member == vchannel.guild.me:
  236. return False
  237.  
  238. if excluding_deaf and any([member.deaf, member.self_deaf]):
  239. return False
  240.  
  241. return True
  242.  
  243. return not sum(1 for m in vchannel.members if check(m))
  244.  
  245. async def _join_startup_channels(self, channels, *, autosummon=True):
  246. joined_servers = set()
  247. channel_map = {c.guild: c for c in channels}
  248.  
  249. def _autopause(player):
  250. if self._check_if_empty(player.voice_client.channel):
  251. log.info("Initial autopause in empty channel")
  252.  
  253. player.pause()
  254. self.server_specific_data[player.voice_client.channel.guild]['auto_paused'] = True
  255.  
  256. for guild in self.guilds:
  257. if guild.unavailable or guild in channel_map:
  258. continue
  259.  
  260. if guild.me.voice:
  261. log.info("Found resumable voice channel {0.guild.name}/{0.name}".format(guild.me.voice.channel))
  262. channel_map[guild] = guild.me.voice.channel
  263.  
  264. if autosummon:
  265. owner = self._get_owner(server=guild, voice=True)
  266. if owner:
  267. log.info("Found owner in \"{}\"".format(owner.voice.channel.name))
  268. channel_map[guild] = owner.voice.channel
  269.  
  270. for guild, channel in channel_map.items():
  271. if guild in joined_servers:
  272. log.info("Already joined a channel in \"{}\", skipping".format(guild.name))
  273. continue
  274.  
  275. if channel and isinstance(channel, discord.VoiceChannel):
  276. log.info("Attempting to join {0.guild.name}/{0.name}".format(channel))
  277.  
  278. chperms = channel.permissions_for(guild.me)
  279.  
  280. if not chperms.connect:
  281. log.info("Cannot join channel \"{}\", no permission.".format(channel.name))
  282. continue
  283.  
  284. elif not chperms.speak:
  285. log.info("Will not join channel \"{}\", no permission to speak.".format(channel.name))
  286. continue
  287.  
  288. try:
  289. player = await self.get_player(channel, create=True, deserialize=self.config.persistent_queue)
  290. joined_servers.add(guild)
  291.  
  292. log.info("Joined {0.guild.name}/{0.name}".format(channel))
  293.  
  294. if player.is_stopped:
  295. player.play()
  296.  
  297. if self.config.auto_playlist:
  298. if self.config.auto_pause:
  299. player.once('play', lambda player, **_: _autopause(player))
  300. if not player.playlist.entries:
  301. await self.on_player_finished_playing(player)
  302.  
  303. except Exception:
  304. log.debug("Error joining {0.guild.name}/{0.name}".format(channel), exc_info=True)
  305. log.error("Failed to join {0.guild.name}/{0.name}".format(channel))
  306.  
  307. elif channel:
  308. log.warning("Not joining {0.guild.name}/{0.name}, that's a text channel.".format(channel))
  309.  
  310. else:
  311. log.warning("Invalid channel thing: {}".format(channel))
  312.  
  313. async def _wait_delete_msg(self, message, after):
  314. await asyncio.sleep(after)
  315. await self.safe_delete_message(message, quiet=True)
  316.  
  317. # TODO: Check to see if I can just move this to on_message after the response check
  318. async def _manual_delete_check(self, message, *, quiet=False):
  319. if self.config.delete_invoking:
  320. await self.safe_delete_message(message, quiet=quiet)
  321.  
  322. async def _check_ignore_non_voice(self, msg):
  323. vc = msg.guild.me.voice.channel
  324.  
  325. # If we've connected to a voice chat and we're in the same voice channel
  326. if not vc or vc == msg.author.voice.channel:
  327. return True
  328. else:
  329. raise exceptions.PermissionsError(
  330. "you cannot use this command when not in the voice channel (%s)" % vc.name, expire_in=30)
  331.  
  332. async def _cache_app_info(self, *, update=False):
  333. if not self.cached_app_info and not update and self.user.bot:
  334. log.debug("Caching app info")
  335. self.cached_app_info = await self.application_info()
  336.  
  337. return self.cached_app_info
  338.  
  339.  
  340. async def remove_from_autoplaylist(self, song_url:str, *, ex:Exception=None, delete_from_ap=False):
  341. if song_url not in self.autoplaylist:
  342. log.debug("URL \"{}\" not in autoplaylist, ignoring".format(song_url))
  343. return
  344.  
  345. async with self.aiolocks[_func_()]:
  346. self.autoplaylist.remove(song_url)
  347. log.info("Removing unplayable song from session autoplaylist: %s" % song_url)
  348.  
  349. with open(self.config.auto_playlist_removed_file, 'a', encoding='utf8') as f:
  350. f.write(
  351. '# Entry removed {ctime}\n'
  352. '# Reason: {ex}\n'
  353. '{url}\n\n{sep}\n\n'.format(
  354. ctime=time.ctime(),
  355. ex=str(ex).replace('\n', '\n#' + ' ' * 10), # 10 spaces to line up with # Reason:
  356. url=song_url,
  357. sep='#' * 32
  358. ))
  359.  
  360. if delete_from_ap:
  361. log.info("Updating autoplaylist")
  362. write_file(self.config.auto_playlist_file, self.autoplaylist)
  363.  
  364. @ensure_appinfo
  365. async def generate_invite_link(self, *, permissions=discord.Permissions(70380544), guild=None):
  366. return discord.utils.oauth_url(self.cached_app_info.id, permissions=permissions, guild=guild)
  367.  
  368. async def get_voice_client(self, channel: discord.abc.GuildChannel):
  369. if isinstance(channel, discord.Object):
  370. channel = self.get_channel(channel.id)
  371.  
  372. if not isinstance(channel, discord.VoiceChannel):
  373. raise AttributeError('Channel passed must be a voice channel')
  374.  
  375. if channel.guild.voice_client:
  376. return channel.guild.voice_client
  377. else:
  378. return await channel.connect(timeout=60, reconnect=True)
  379.  
  380. async def disconnect_voice_client(self, guild):
  381. vc = self.voice_client_in(guild)
  382. if not vc:
  383. return
  384.  
  385. if guild.id in self.players:
  386. self.players.pop(guild.id).kill()
  387.  
  388. await vc.disconnect()
  389.  
  390. async def disconnect_all_voice_clients(self):
  391. for vc in list(self.voice_clients).copy():
  392. await self.disconnect_voice_client(vc.channel.guild)
  393.  
  394. async def set_voice_state(self, vchannel, *, mute=False, deaf=False):
  395. if isinstance(vchannel, discord.Object):
  396. vchannel = self.get_channel(vchannel.id)
  397.  
  398. if getattr(vchannel, 'type', ChannelType.text) != ChannelType.voice:
  399. raise AttributeError('Channel passed must be a voice channel')
  400.  
  401. await self.ws.voice_state(vchannel.guild.id, vchannel.id, mute, deaf)
  402. # I hope I don't have to set the channel here
  403. # instead of waiting for the event to update it
  404.  
  405. def get_player_in(self, guild:discord.Guild) -> MusicPlayer:
  406. return self.players.get(guild.id)
  407.  
  408. async def get_player(self, channel, create=False, *, deserialize=False) -> MusicPlayer:
  409. guild = channel.guild
  410.  
  411. async with self.aiolocks[_func_() + ':' + str(guild.id)]:
  412. if deserialize:
  413. voice_client = await self.get_voice_client(channel)
  414. player = await self.deserialize_queue(guild, voice_client)
  415.  
  416. if player:
  417. log.debug("Created player via deserialization for guild %s with %s entries", guild.id, len(player.playlist))
  418. # Since deserializing only happens when the bot starts, I should never need to reconnect
  419. return self._init_player(player, guild=guild)
  420.  
  421. if guild.id not in self.players:
  422. if not create:
  423. raise exceptions.CommandError(
  424. 'The bot is not in a voice channel. '
  425. 'Use %ssummon to summon it to your voice channel.' % self.config.command_prefix)
  426.  
  427. voice_client = await self.get_voice_client(channel)
  428.  
  429. playlist = Playlist(self)
  430. player = MusicPlayer(self, voice_client, playlist)
  431. self._init_player(player, guild=guild)
  432.  
  433. return self.players[guild.id]
  434.  
  435. def _init_player(self, player, *, guild=None):
  436. player = player.on('play', self.on_player_play) \
  437. .on('resume', self.on_player_resume) \
  438. .on('pause', self.on_player_pause) \
  439. .on('stop', self.on_player_stop) \
  440. .on('finished-playing', self.on_player_finished_playing) \
  441. .on('entry-added', self.on_player_entry_added) \
  442. .on('error', self.on_player_error)
  443.  
  444. player.skip_state = SkipState()
  445.  
  446. if guild:
  447. self.players[guild.id] = player
  448.  
  449. return player
  450.  
  451. async def on_player_play(self, player, entry):
  452. log.debug('Running on_player_play')
  453. await self.update_now_playing_status(entry)
  454. player.skip_state.reset()
  455.  
  456. # This is the one event where its ok to serialize autoplaylist entries
  457. await self.serialize_queue(player.voice_client.channel.guild)
  458.  
  459. if self.config.write_current_song:
  460. await self.write_current_song(player.voice_client.channel.guild, entry)
  461.  
  462. channel = entry.meta.get('channel', None)
  463. author = entry.meta.get('author', None)
  464.  
  465. if channel and author:
  466. last_np_msg = self.server_specific_data[channel.guild]['last_np_msg']
  467. if last_np_msg and last_np_msg.channel == channel:
  468.  
  469. async for lmsg in channel.history(limit=1):
  470. if lmsg != last_np_msg and last_np_msg:
  471. await self.safe_delete_message(last_np_msg)
  472. self.server_specific_data[channel.guild]['last_np_msg'] = None
  473. break # This is probably redundant
  474.  
  475. author_perms = self.permissions.for_user(author)
  476.  
  477. if author not in player.voice_client.channel.members and author_perms.skip_when_absent:
  478. newmsg = 'Skipping next song in `%s`: `%s` added by `%s` as queuer not in voice' % (
  479. player.voice_client.channel.name, entry.title, entry.meta['author'].name)
  480. player.skip()
  481. elif self.config.now_playing_mentions:
  482. newmsg = '%s - your song `%s` is now playing in `%s`!' % (
  483. entry.meta['author'].mention, entry.title, player.voice_client.channel.name)
  484. else:
  485. newmsg = 'Now playing in `%s`: `%s` added by `%s`' % (
  486. player.voice_client.channel.name, entry.title, entry.meta['author'].name)
  487.  
  488. if self.server_specific_data[channel.guild]['last_np_msg']:
  489. self.server_specific_data[channel.guild]['last_np_msg'] = await self.safe_edit_message(last_np_msg, newmsg, send_if_fail=True)
  490. else:
  491. self.server_specific_data[channel.guild]['last_np_msg'] = await self.safe_send_message(channel, newmsg)
  492.  
  493. # TODO: Check channel voice state?
  494.  
  495. async def on_player_resume(self, player, entry, **_):
  496. log.debug('Running on_player_resume')
  497. await self.update_now_playing_status(entry)
  498.  
  499. async def on_player_pause(self, player, entry, **_):
  500. log.debug('Running on_player_pause')
  501. await self.update_now_playing_status(entry, True)
  502. # await self.serialize_queue(player.voice_client.channel.guild)
  503.  
  504. async def on_player_stop(self, player, **_):
  505. log.debug('Running on_player_stop')
  506. await self.update_now_playing_status()
  507.  
  508. async def on_player_finished_playing(self, player, **_):
  509. log.debug('Running on_player_finished_playing')
  510. def _autopause(player):
  511. if self._check_if_empty(player.voice_client.channel):
  512. log.info("Player finished playing, autopaused in empty channel")
  513.  
  514. player.pause()
  515. self.server_specific_data[player.voice_client.channel.guild]['auto_paused'] = True
  516.  
  517. if not player.playlist.entries and not player.current_entry and self.config.auto_playlist:
  518. if not player.autoplaylist:
  519. if not self.autoplaylist:
  520. # TODO: When I add playlist expansion, make sure that's not happening during this check
  521. log.warning("No playable songs in the autoplaylist, disabling.")
  522. self.config.auto_playlist = False
  523. else:
  524. log.debug("No content in current autoplaylist. Filling with new music...")
  525. player.autoplaylist = list(self.autoplaylist)
  526.  
  527. while player.autoplaylist:
  528. if self.config.auto_playlist_random:
  529. random.shuffle(player.autoplaylist)
  530. song_url = random.choice(player.autoplaylist)
  531. else:
  532. song_url = player.autoplaylist[0]
  533. player.autoplaylist.remove(song_url)
  534.  
  535. info = {}
  536.  
  537. try:
  538. info = await self.downloader.extract_info(player.playlist.loop, song_url, download=False, process=False)
  539. except downloader.youtube_dl.utils.DownloadError as e:
  540. if 'YouTube said:' in e.args[0]:
  541. # url is bork, remove from list and put in removed list
  542. log.error("Error processing youtube url:\n{}".format(e.args[0]))
  543.  
  544. else:
  545. # Probably an error from a different extractor, but I've only seen youtube's
  546. log.error("Error processing \"{url}\": {ex}".format(url=song_url, ex=e))
  547.  
  548. await self.remove_from_autoplaylist(song_url, ex=e, delete_from_ap=self.config.remove_ap)
  549. continue
  550.  
  551. except Exception as e:
  552. log.error("Error processing \"{url}\": {ex}".format(url=song_url, ex=e))
  553. log.exception()
  554.  
  555. self.autoplaylist.remove(song_url)
  556. continue
  557.  
  558. if info.get('entries', None): # or .get('_type', '') == 'playlist'
  559. log.debug("Playlist found but is unsupported at this time, skipping.")
  560. # TODO: Playlist expansion
  561.  
  562. # Do I check the initial conditions again?
  563. # not (not player.playlist.entries and not player.current_entry and self.config.auto_playlist)
  564.  
  565. if self.config.auto_pause:
  566. player.once('play', lambda player, **_: _autopause(player))
  567.  
  568. try:
  569. await player.playlist.add_entry(song_url, channel=None, author=None)
  570. except exceptions.ExtractionError as e:
  571. log.error("Error adding song from autoplaylist: {}".format(e))
  572. log.debug('', exc_info=True)
  573. continue
  574.  
  575. break
  576.  
  577. if not self.autoplaylist:
  578. # TODO: When I add playlist expansion, make sure that's not happening during this check
  579. log.warning("No playable songs in the autoplaylist, disabling.")
  580. self.config.auto_playlist = False
  581.  
  582. else: # Don't serialize for autoplaylist events
  583. await self.serialize_queue(player.voice_client.channel.guild)
  584.  
  585. async def on_player_entry_added(self, player, playlist, entry, **_):
  586. log.debug('Running on_player_entry_added')
  587. if entry.meta.get('author') and entry.meta.get('channel'):
  588. await self.serialize_queue(player.voice_client.channel.guild)
  589.  
  590. async def on_player_error(self, player, entry, ex, **_):
  591. if 'channel' in entry.meta:
  592. await self.safe_send_message(
  593. entry.meta['channel'],
  594. "```\nError from FFmpeg:\n{}\n```".format(ex)
  595. )
  596. else:
  597. log.exception("Player error", exc_info=ex)
  598.  
  599. async def update_now_playing_status(self, entry=None, is_paused=False):
  600. game = None
  601.  
  602. if not self.config.status_message:
  603. if self.user.bot:
  604. activeplayers = sum(1 for p in self.players.values() if p.is_playing)
  605. if activeplayers > 1:
  606. game = discord.Game(type=0, name="music on %s guilds" % activeplayers)
  607. entry = None
  608.  
  609. elif activeplayers == 1:
  610. player = discord.utils.get(self.players.values(), is_playing=True)
  611. entry = player.current_entry
  612.  
  613. if entry:
  614. prefix = u'\u275A\u275A ' if is_paused else ''
  615.  
  616. name = u'{}{}'.format(prefix, entry.title)[:128]
  617. game = discord.Game(type=0, name=name)
  618. else:
  619. game = discord.Game(type=0, name=self.config.status_message.strip()[:128])
  620.  
  621. async with self.aiolocks[_func_()]:
  622. if game != self.last_status:
  623. await self.change_presence(activity=game)
  624. self.last_status = game
  625.  
  626. async def update_now_playing_message(self, guild, message, *, channel=None):
  627. lnp = self.server_specific_data[guild]['last_np_msg']
  628. m = None
  629.  
  630. if message is None and lnp:
  631. await self.safe_delete_message(lnp, quiet=True)
  632.  
  633. elif lnp: # If there was a previous lp message
  634. oldchannel = lnp.channel
  635.  
  636. if lnp.channel == oldchannel: # If we have a channel to update it in
  637. async for lmsg in self.logs_from(channel, limit=1):
  638. if lmsg != lnp and lnp: # If we need to resend it
  639. await self.safe_delete_message(lnp, quiet=True)
  640. m = await self.safe_send_message(channel, message, quiet=True)
  641. else:
  642. m = await self.safe_edit_message(lnp, message, send_if_fail=True, quiet=False)
  643.  
  644. elif channel: # If we have a new channel to send it to
  645. await self.safe_delete_message(lnp, quiet=True)
  646. m = await self.safe_send_message(channel, message, quiet=True)
  647.  
  648. else: # we just resend it in the old channel
  649. await self.safe_delete_message(lnp, quiet=True)
  650. m = await self.safe_send_message(oldchannel, message, quiet=True)
  651.  
  652. elif channel: # No previous message
  653. m = await self.safe_send_message(channel, message, quiet=True)
  654.  
  655. self.server_specific_data[guild]['last_np_msg'] = m
  656.  
  657.  
  658. async def serialize_queue(self, guild, *, dir=None):
  659. """
  660. Serialize the current queue for a server's player to json.
  661. """
  662.  
  663. player = self.get_player_in(guild)
  664. if not player:
  665. return
  666.  
  667. if dir is None:
  668. dir = 'data/%s/queue.json' % guild.id
  669.  
  670. async with self.aiolocks['queue_serialization' + ':' + str(guild.id)]:
  671. log.debug("Serializing queue for %s", guild.id)
  672.  
  673. with open(dir, 'w', encoding='utf8') as f:
  674. f.write(player.serialize(sort_keys=True))
  675.  
  676. async def serialize_all_queues(self, *, dir=None):
  677. coros = [self.serialize_queue(s, dir=dir) for s in self.guilds]
  678. await asyncio.gather(*coros, return_exceptions=True)
  679.  
  680. async def deserialize_queue(self, guild, voice_client, playlist=None, *, dir=None) -> MusicPlayer:
  681. """
  682. Deserialize a saved queue for a server into a MusicPlayer. If no queue is saved, returns None.
  683. """
  684.  
  685. if playlist is None:
  686. playlist = Playlist(self)
  687.  
  688. if dir is None:
  689. dir = 'data/%s/queue.json' % guild.id
  690.  
  691. async with self.aiolocks['queue_serialization' + ':' + str(guild.id)]:
  692. if not os.path.isfile(dir):
  693. return None
  694.  
  695. log.debug("Deserializing queue for %s", guild.id)
  696.  
  697. with open(dir, 'r', encoding='utf8') as f:
  698. data = f.read()
  699.  
  700. return MusicPlayer.from_json(data, self, voice_client, playlist)
  701.  
  702. async def write_current_song(self, guild, entry, *, dir=None):
  703. """
  704. Writes the current song to file
  705. """
  706. player = self.get_player_in(guild)
  707. if not player:
  708. return
  709.  
  710. if dir is None:
  711. dir = 'data/%s/current.txt' % guild.id
  712.  
  713. async with self.aiolocks['current_song' + ':' + str(guild.id)]:
  714. log.debug("Writing current song for %s", guild.id)
  715.  
  716. with open(dir, 'w', encoding='utf8') as f:
  717. f.write(entry.title)
  718.  
  719. @ensure_appinfo
  720. async def _on_ready_sanity_checks(self):
  721. # Ensure folders exist
  722. await self._scheck_ensure_env()
  723.  
  724. # Server permissions check
  725. await self._scheck_server_permissions()
  726.  
  727. # playlists in autoplaylist
  728. await self._scheck_autoplaylist()
  729.  
  730. # config/permissions async validate?
  731. await self._scheck_configs()
  732.  
  733.  
  734. async def _scheck_ensure_env(self):
  735. log.debug("Ensuring data folders exist")
  736. for guild in self.guilds:
  737. pathlib.Path('data/%s/' % guild.id).mkdir(exist_ok=True)
  738.  
  739. with open('data/server_names.txt', 'w', encoding='utf8') as f:
  740. for guilds in sorted(self.guilds, key=lambda s:int(s.id)):
  741. f.write('{:<22} {}\n'.format(guild.id, guild.name))
  742.  
  743. if not self.config.save_videos and os.path.isdir(AUDIO_CACHE_PATH):
  744. if self._delete_old_audiocache():
  745. log.debug("Deleted old audio cache")
  746. else:
  747. log.debug("Could not delete old audio cache, moving on.")
  748.  
  749.  
  750. async def _scheck_server_permissions(self):
  751. log.debug("Checking server permissions")
  752. pass # TODO
  753.  
  754. async def _scheck_autoplaylist(self):
  755. log.debug("Auditing autoplaylist")
  756. pass # TODO
  757.  
  758. async def _scheck_configs(self):
  759. log.debug("Validating config")
  760. await self.config.async_validate(self)
  761.  
  762. log.debug("Validating permissions config")
  763. await self.permissions.async_validate(self)
  764.  
  765.  
  766.  
  767. #######################################################################################################################
  768.  
  769.  
  770. async def safe_send_message(self, dest, content, **kwargs):
  771. tts = kwargs.pop('tts', False)
  772. quiet = kwargs.pop('quiet', False)
  773. expire_in = kwargs.pop('expire_in', 0)
  774. allow_none = kwargs.pop('allow_none', True)
  775. also_delete = kwargs.pop('also_delete', None)
  776.  
  777. msg = None
  778. lfunc = log.debug if quiet else log.warning
  779.  
  780. try:
  781. if content is not None or allow_none:
  782. if isinstance(content, discord.Embed):
  783. msg = await dest.send(embed=content)
  784. else:
  785. msg = await dest.send(content, tts=tts)
  786.  
  787. except discord.Forbidden:
  788. lfunc("Cannot send message to \"%s\", no permission", dest.name)
  789.  
  790. except discord.NotFound:
  791. lfunc("Cannot send message to \"%s\", invalid channel?", dest.name)
  792.  
  793. except discord.HTTPException:
  794. if len(content) > DISCORD_MSG_CHAR_LIMIT:
  795. lfunc("Message is over the message size limit (%s)", DISCORD_MSG_CHAR_LIMIT)
  796. else:
  797. lfunc("Failed to send message")
  798. log.noise("Got HTTPException trying to send message to %s: %s", dest, content)
  799.  
  800. finally:
  801. if msg and expire_in:
  802. asyncio.ensure_future(self._wait_delete_msg(msg, expire_in))
  803.  
  804. if also_delete and isinstance(also_delete, discord.Message):
  805. asyncio.ensure_future(self._wait_delete_msg(also_delete, expire_in))
  806.  
  807. return msg
  808.  
  809. async def safe_delete_message(self, message, *, quiet=False):
  810. lfunc = log.debug if quiet else log.warning
  811.  
  812. try:
  813. return await message.delete()
  814.  
  815. except discord.Forbidden:
  816. lfunc("Cannot delete message \"{}\", no permission".format(message.clean_content))
  817.  
  818. except discord.NotFound:
  819. lfunc("Cannot delete message \"{}\", message not found".format(message.clean_content))
  820.  
  821. async def safe_edit_message(self, message, new, *, send_if_fail=False, quiet=False):
  822. lfunc = log.debug if quiet else log.warning
  823.  
  824. try:
  825. return await message.edit(content=new)
  826.  
  827. except discord.NotFound:
  828. lfunc("Cannot edit message \"{}\", message not found".format(message.clean_content))
  829. if send_if_fail:
  830. lfunc("Sending message instead")
  831. return await self.safe_send_message(message.channel, new)
  832.  
  833. async def send_typing(self, destination):
  834. try:
  835. return await destination.trigger_typing()
  836. except discord.Forbidden:
  837. log.warning("Could not send typing to {}, no permission".format(destination))
  838.  
  839. async def restart(self):
  840. self.exit_signal = exceptions.RestartSignal()
  841. await self.logout()
  842.  
  843. def restart_threadsafe(self):
  844. asyncio.run_coroutine_threadsafe(self.restart(), self.loop)
  845.  
  846. def _cleanup(self):
  847. try:
  848. self.loop.run_until_complete(self.logout())
  849. self.loop.run_until_complete(self.aiosession.close())
  850. except: pass
  851.  
  852. pending = asyncio.Task.all_tasks()
  853. gathered = asyncio.gather(*pending)
  854.  
  855. try:
  856. gathered.cancel()
  857. self.loop.run_until_complete(gathered)
  858. gathered.exception()
  859. except: pass
  860.  
  861. # noinspection PyMethodOverriding
  862. def run(self):
  863. try:
  864. self.loop.run_until_complete(self.start(*self.config.auth))
  865.  
  866. except discord.errors.LoginFailure:
  867. # Add if token, else
  868. raise exceptions.HelpfulError(
  869. "Bot cannot login, bad credentials.",
  870. "Fix your token in the options file. "
  871. "Remember that each field should be on their own line."
  872. ) # ^^^^ In theory self.config.auth should never have no items
  873.  
  874. finally:
  875. try:
  876. self._cleanup()
  877. except Exception:
  878. log.error("Error in cleanup", exc_info=True)
  879.  
  880. if self.exit_signal:
  881. raise self.exit_signal
  882.  
  883. async def logout(self):
  884. await self.disconnect_all_voice_clients()
  885. return await super().logout()
  886.  
  887. async def on_error(self, event, *args, **kwargs):
  888. ex_type, ex, stack = sys.exc_info()
  889.  
  890. if ex_type == exceptions.HelpfulError:
  891. log.error("Exception in {}:\n{}".format(event, ex.message))
  892.  
  893. await asyncio.sleep(2) # don't ask
  894. await self.logout()
  895.  
  896. elif issubclass(ex_type, exceptions.Signal):
  897. self.exit_signal = ex_type
  898. await self.logout()
  899.  
  900. else:
  901. log.error("Exception in {}".format(event), exc_info=True)
  902.  
  903. async def on_resumed(self):
  904. log.info("\nReconnected to discord.\n")
  905.  
  906. async def on_ready(self):
  907. dlogger = logging.getLogger('discord')
  908. for h in dlogger.handlers:
  909. if getattr(h, 'terminator', None) == '':
  910. dlogger.removeHandler(h)
  911. print()
  912.  
  913. log.debug("Connection established, ready to go.")
  914.  
  915. self.ws._keep_alive.name = 'Gateway Keepalive'
  916.  
  917. if self.init_ok:
  918. log.debug("Received additional READY event, may have failed to resume")
  919. return
  920.  
  921. await self._on_ready_sanity_checks()
  922.  
  923. self.init_ok = True
  924.  
  925. ################################
  926.  
  927. log.info("Connected: {0}/{1}#{2}".format(
  928. self.user.id,
  929. self.user.name,
  930. self.user.discriminator
  931. ))
  932.  
  933. owner = self._get_owner(voice=True) or self._get_owner()
  934. if owner and self.guilds:
  935. log.info("Owner: {0}/{1}#{2}\n".format(
  936. owner.id,
  937. owner.name,
  938. owner.discriminator
  939. ))
  940.  
  941. log.info('Guild List:')
  942. for s in self.guilds:
  943. ser = ('{} (unavailable)'.format(s.name) if s.unavailable else s.name)
  944. log.info(' - ' + ser)
  945.  
  946. elif self.guilds:
  947. log.warning("Owner could not be found on any guild (id: %s)\n" % self.config.owner_id)
  948.  
  949. log.info('Guild List:')
  950. for s in self.guilds:
  951. ser = ('{} (unavailable)'.format(s.name) if s.unavailable else s.name)
  952. log.info(' - ' + ser)
  953.  
  954. else:
  955. log.warning("Owner unknown, bot is not on any guilds.")
  956. if self.user.bot:
  957. log.warning(
  958. "To make the bot join a guild, paste this link in your browser. \n"
  959. "Note: You should be logged into your main account and have \n"
  960. "manage server permissions on the guild you want the bot to join.\n"
  961. " " + await self.generate_invite_link()
  962. )
  963.  
  964. print(flush=True)
  965.  
  966. if self.config.bound_channels:
  967. chlist = set(self.get_channel(i) for i in self.config.bound_channels if i)
  968. chlist.discard(None)
  969.  
  970. invalids = set()
  971. invalids.update(c for c in chlist if isinstance(c, discord.VoiceChannel))
  972.  
  973. chlist.difference_update(invalids)
  974. self.config.bound_channels.difference_update(invalids)
  975.  
  976. if chlist:
  977. log.info("Bound to text channels:")
  978. [log.info(' - {}/{}'.format(ch.guild.name.strip(), ch.name.strip())) for ch in chlist if ch]
  979. else:
  980. print("Not bound to any text channels")
  981.  
  982. if invalids and self.config.debug_mode:
  983. print(flush=True)
  984. log.info("Not binding to voice channels:")
  985. [log.info(' - {}/{}'.format(ch.guild.name.strip(), ch.name.strip())) for ch in invalids if ch]
  986.  
  987. print(flush=True)
  988.  
  989. else:
  990. log.info("Not bound to any text channels")
  991.  
  992. if self.config.autojoin_channels:
  993. chlist = set(self.get_channel(i) for i in self.config.autojoin_channels if i)
  994. chlist.discard(None)
  995.  
  996. invalids = set()
  997. invalids.update(c for c in chlist if isinstance(c, discord.TextChannel))
  998.  
  999. chlist.difference_update(invalids)
  1000. self.config.autojoin_channels.difference_update(invalids)
  1001.  
  1002. if chlist:
  1003. log.info("Autojoining voice chanels:")
  1004. [log.info(' - {}/{}'.format(ch.guild.name.strip(), ch.name.strip())) for ch in chlist if ch]
  1005. else:
  1006. log.info("Not autojoining any voice channels")
  1007.  
  1008. if invalids and self.config.debug_mode:
  1009. print(flush=True)
  1010. log.info("Cannot autojoin text channels:")
  1011. [log.info(' - {}/{}'.format(ch.guild.name.strip(), ch.name.strip())) for ch in invalids if ch]
  1012.  
  1013. self.autojoin_channels = chlist
  1014.  
  1015. else:
  1016. log.info("Not autojoining any voice channels")
  1017. self.autojoin_channels = set()
  1018.  
  1019. if self.config.show_config_at_start:
  1020. print(flush=True)
  1021. log.info("Options:")
  1022.  
  1023. log.info(" Command prefix: " + self.config.command_prefix)
  1024. log.info(" Default volume: {}%".format(int(self.config.default_volume * 100)))
  1025. log.info(" Skip threshold: {} votes or {}%".format(
  1026. self.config.skips_required, fixg(self.config.skip_ratio_required * 100)))
  1027. log.info(" Now Playing @mentions: " + ['Disabled', 'Enabled'][self.config.now_playing_mentions])
  1028. log.info(" Auto-Summon: " + ['Disabled', 'Enabled'][self.config.auto_summon])
  1029. log.info(" Auto-Playlist: " + ['Disabled', 'Enabled'][self.config.auto_playlist] + " (order: " + ['sequential', 'random'][self.config.auto_playlist_random] + ")")
  1030. log.info(" Auto-Pause: " + ['Disabled', 'Enabled'][self.config.auto_pause])
  1031. log.info(" Delete Messages: " + ['Disabled', 'Enabled'][self.config.delete_messages])
  1032. if self.config.delete_messages:
  1033. log.info(" Delete Invoking: " + ['Disabled', 'Enabled'][self.config.delete_invoking])
  1034. log.info(" Debug Mode: " + ['Disabled', 'Enabled'][self.config.debug_mode])
  1035. log.info(" Downloaded songs will be " + ['deleted', 'saved'][self.config.save_videos])
  1036. if self.config.status_message:
  1037. log.info(" Status message: " + self.config.status_message)
  1038. log.info(" Write current songs to file: " + ['Disabled', 'Enabled'][self.config.write_current_song])
  1039. log.info(" Author insta-skip: " + ['Disabled', 'Enabled'][self.config.allow_author_skip])
  1040. log.info(" Embeds: " + ['Disabled', 'Enabled'][self.config.embeds])
  1041. log.info(" Spotify integration: " + ['Disabled', 'Enabled'][self.config._spotify])
  1042. log.info(" Legacy skip: " + ['Disabled', 'Enabled'][self.config.legacy_skip])
  1043.  
  1044. print(flush=True)
  1045.  
  1046. await self.update_now_playing_status()
  1047.  
  1048. # maybe option to leave the ownerid blank and generate a random command for the owner to use
  1049. # wait_for_message is pretty neato
  1050.  
  1051. await self._join_startup_channels(self.autojoin_channels, autosummon=self.config.auto_summon)
  1052.  
  1053. # we do this after the config stuff because it's a lot easier to notice here
  1054. if self.config.missing_keys:
  1055. log.warning('Your config file is missing some options. If you have recently updated, '
  1056. 'check the example_options.ini file to see if there are new options available to you. '
  1057. 'The options missing are: {0}'.format(self.config.missing_keys))
  1058. print(flush=True)
  1059.  
  1060. # t-t-th-th-that's all folks!
  1061.  
  1062. def _gen_embed(self):
  1063. """Provides a basic template for embeds"""
  1064. e = discord.Embed()
  1065. e.colour = 7506394
  1066. e.set_footer(text='Just-Some-Bots/MusicBot ({})'.format(BOTVERSION), icon_url='https://i.imgur.com/gFHBoZA.png')
  1067. e.set_author(name=self.user.name, url='https://github.com/Just-Some-Bots/MusicBot', icon_url=self.user.avatar_url)
  1068. return e
  1069.  
  1070. async def cmd_resetplaylist(self, player, channel):
  1071. """
  1072. Usage:
  1073. {command_prefix}resetplaylist
  1074.  
  1075. Resets all songs in the server's autoplaylist
  1076. """
  1077. player.autoplaylist = list(set(self.autoplaylist))
  1078. return Response(self.str.get('cmd-resetplaylist-response', '\N{OK HAND SIGN}'), delete_after=15)
  1079.  
  1080. async def cmd_help(self, message, channel, command=None):
  1081. """
  1082. Usage:
  1083. {command_prefix}help [command]
  1084.  
  1085. Prints a help message.
  1086. If a command is specified, it prints a help message for that command.
  1087. Otherwise, it lists the available commands.
  1088. """
  1089. self.commands = []
  1090. self.is_all = False
  1091. prefix = self.config.command_prefix
  1092.  
  1093. if command:
  1094. if command.lower() == 'all':
  1095. self.is_all = True
  1096. await self.gen_cmd_list(message, list_all_cmds=True)
  1097.  
  1098. else:
  1099. cmd = getattr(self, 'cmd_' + command, None)
  1100. if cmd and not hasattr(cmd, 'dev_cmd'):
  1101. return Response(
  1102. "```\n{}```".format(
  1103. dedent(cmd.__doc__)
  1104. ).format(command_prefix=self.config.command_prefix),
  1105. delete_after=60
  1106. )
  1107. else:
  1108. raise exceptions.CommandError(self.str.get('cmd-help-invalid', "No such command"), expire_in=10)
  1109.  
  1110. elif message.author.id == self.config.owner_id:
  1111. await self.gen_cmd_list(message, list_all_cmds=True)
  1112.  
  1113. else:
  1114. await self.gen_cmd_list(message)
  1115.  
  1116. desc = '```\n' + ', '.join(self.commands) + '\n```\n' + self.str.get(
  1117. 'cmd-help-response', 'For information about a particular command, run `{}help [command]`\n'
  1118. 'For further help, see https://just-some-bots.github.io/MusicBot/').format(prefix)
  1119. if not self.is_all:
  1120. desc += self.str.get('cmd-help-all', '\nOnly showing commands you can use, for a list of all commands, run `{}help all`').format(prefix)
  1121.  
  1122. return Response(desc, reply=True, delete_after=60)
  1123.  
  1124. async def cmd_blacklist(self, message, user_mentions, option, something):
  1125. """
  1126. Usage:
  1127. {command_prefix}blacklist [ + | - | add | remove ] @UserName [@UserName2 ...]
  1128.  
  1129. Add or remove users to the blacklist.
  1130. Blacklisted users are forbidden from using bot commands.
  1131. """
  1132.  
  1133. if not user_mentions:
  1134. raise exceptions.CommandError("No users listed.", expire_in=20)
  1135.  
  1136. if option not in ['+', '-', 'add', 'remove']:
  1137. raise exceptions.CommandError(
  1138. self.str.get('cmd-blacklist-invalid', 'Invalid option "{0}" specified, use +, -, add, or remove').format(option), expire_in=20
  1139. )
  1140.  
  1141. for user in user_mentions.copy():
  1142. if user.id == self.config.owner_id:
  1143. print("[Commands:Blacklist] The owner cannot be blacklisted.")
  1144. user_mentions.remove(user)
  1145.  
  1146. old_len = len(self.blacklist)
  1147.  
  1148. if option in ['+', 'add']:
  1149. self.blacklist.update(user.id for user in user_mentions)
  1150.  
  1151. write_file(self.config.blacklist_file, self.blacklist)
  1152.  
  1153. return Response(
  1154. self.str.get('cmd-blacklist-added', '{0} users have been added to the blacklist').format(len(self.blacklist) - old_len),
  1155. reply=True, delete_after=10
  1156. )
  1157.  
  1158. else:
  1159. if self.blacklist.isdisjoint(user.id for user in user_mentions):
  1160. return Response(self.str.get('cmd-blacklist-none', 'None of those users are in the blacklist.'), reply=True, delete_after=10)
  1161.  
  1162. else:
  1163. self.blacklist.difference_update(user.id for user in user_mentions)
  1164. write_file(self.config.blacklist_file, self.blacklist)
  1165.  
  1166. return Response(
  1167. self.str.get('cmd-blacklist-removed', '{0} users have been removed from the blacklist').format(old_len - len(self.blacklist)),
  1168. reply=True, delete_after=10
  1169. )
  1170.  
  1171. async def cmd_id(self, author, user_mentions):
  1172. """
  1173. Usage:
  1174. {command_prefix}id [@user]
  1175.  
  1176. Tells the user their id or the id of another user.
  1177. """
  1178. if not user_mentions:
  1179. return Response(self.str.get('cmd-id-self', 'Your ID is `{0}`').format(author.id), reply=True, delete_after=35)
  1180. else:
  1181. usr = user_mentions[0]
  1182. return Response(self.str.get('cmd-id-other', '**{0}**s ID is `{1}`').format(usr.name, usr.id), reply=True, delete_after=35)
  1183.  
  1184. async def cmd_save(self, player, url=None):
  1185. """
  1186. Usage:
  1187. {command_prefix}save [url]
  1188.  
  1189. Saves the specified song or current song if not specified to the autoplaylist.
  1190. """
  1191. if url or (player.current_entry and not isinstance(player.current_entry, StreamPlaylistEntry)):
  1192. if not url:
  1193. url = player.current_entry.url
  1194.  
  1195. if url not in self.autoplaylist:
  1196. self.autoplaylist.append(url)
  1197. write_file(self.config.auto_playlist_file, self.autoplaylist)
  1198. log.debug("Appended {} to autoplaylist".format(url))
  1199. return Response(self.str.get('cmd-save-success', 'Added <{0}> to the autoplaylist.').format(url))
  1200. else:
  1201. raise exceptions.CommandError(self.str.get('cmd-save-exists', 'This song is already in the autoplaylist.'))
  1202. else:
  1203. raise exceptions.CommandError(self.str.get('cmd-save-invalid', 'There is no valid song playing.'))
  1204.  
  1205. @owner_only
  1206. async def cmd_joinserver(self, message, server_link=None):
  1207. """
  1208. Usage:
  1209. {command_prefix}joinserver invite_link
  1210.  
  1211. Asks the bot to join a server. Note: Bot accounts cannot use invite links.
  1212. """
  1213.  
  1214. url = await self.generate_invite_link()
  1215. return Response(
  1216. self.str.get('cmd-joinserver-response', "Click here to add me to a server: \n{}").format(url),
  1217. reply=True, delete_after=30
  1218. )
  1219.  
  1220. async def cmd_karaoke(self, player, channel, author):
  1221. """
  1222. Usage:
  1223. {command_prefix}karaoke
  1224.  
  1225. Activates karaoke mode. During karaoke mode, only groups with the BypassKaraokeMode
  1226. permission in the config file can queue music.
  1227. """
  1228. player.karaoke_mode = not player.karaoke_mode
  1229. return Response("\N{OK HAND SIGN} Karaoke mode is now " + ['disabled', 'enabled'][player.karaoke_mode], delete_after=15)
  1230.  
  1231. async def _do_playlist_checks(self, permissions, player, author, testobj):
  1232. num_songs = sum(1 for _ in testobj)
  1233.  
  1234. # I have to do exe extra checks anyways because you can request an arbitrary number of search results
  1235. if not permissions.allow_playlists and num_songs > 1:
  1236. raise exceptions.PermissionsError(self.str.get('playlists-noperms', "You are not allowed to request playlists"), expire_in=30)
  1237.  
  1238. if permissions.max_playlist_length and num_songs > permissions.max_playlist_length:
  1239. raise exceptions.PermissionsError(
  1240. self.str.get('playlists-big', "Playlist has too many entries ({0} > {1})").format(num_songs, permissions.max_playlist_length),
  1241. expire_in=30
  1242. )
  1243.  
  1244. # This is a little bit weird when it says (x + 0 > y), I might add the other check back in
  1245. if permissions.max_songs and player.playlist.count_for_user(author) + num_songs > permissions.max_songs:
  1246. raise exceptions.PermissionsError(
  1247. self.str.get('playlists-limit', "Playlist entries + your already queued songs reached limit ({0} + {1} > {2})").format(
  1248. num_songs, player.playlist.count_for_user(author), permissions.max_songs),
  1249. expire_in=30
  1250. )
  1251. return True
  1252.  
  1253. async def cmd_play(self, message, player, channel, author, permissions, leftover_args, song_url):
  1254. """
  1255. Usage:
  1256. {command_prefix}play song_link
  1257. {command_prefix}play text to search for
  1258. {command_prefix}play spotify_uri
  1259.  
  1260. Adds the song to the playlist. If a link is not provided, the first
  1261. result from a youtube search is added to the queue.
  1262.  
  1263. If enabled in the config, the bot will also support Spotify URIs, however
  1264. it will use the metadata (e.g song name and artist) to find a YouTube
  1265. equivalent of the song. Streaming from Spotify is not possible.
  1266. """
  1267.  
  1268. song_url = song_url.strip('<>')
  1269.  
  1270. await self.send_typing(channel)
  1271.  
  1272. if leftover_args:
  1273. song_url = ' '.join([song_url, *leftover_args])
  1274. leftover_args = None # prevent some crazy shit happening down the line
  1275.  
  1276. # Make sure forward slashes work properly in search queries
  1277. linksRegex = '((http(s)*:[/][/]|www.)([a-z]|[A-Z]|[0-9]|[/.]|[~])*)'
  1278. pattern = re.compile(linksRegex)
  1279. matchUrl = pattern.match(song_url)
  1280. song_url = song_url.replace('/', '%2F') if matchUrl is None else song_url
  1281.  
  1282. # Rewrite YouTube playlist URLs if the wrong URL type is given
  1283. playlistRegex = r'watch\?v=.+&(list=[^&]+)'
  1284. matches = re.search(playlistRegex, song_url)
  1285. groups = matches.groups() if matches is not None else []
  1286. song_url = "https://www.youtube.com/playlist?" + groups[0] if len(groups) > 0 else song_url
  1287.  
  1288. if self.config._spotify:
  1289. if 'open.spotify.com' in song_url:
  1290. song_url = 'spotify:' + re.sub('(http[s]?:\/\/)?(open.spotify.com)\/', '', song_url).replace('/', ':')
  1291. if song_url.startswith('spotify:'):
  1292. parts = song_url.split(":")
  1293. try:
  1294. if 'track' in parts:
  1295. res = await self.spotify.get_track(parts[-1])
  1296. song_url = res['artists'][0]['name'] + ' ' + res['name']
  1297. elif 'album' in parts:
  1298. res = await self.spotify.get_album(parts[-1])
  1299. await self._do_playlist_checks(permissions, player, author, res['tracks']['items'])
  1300. procmesg = await self.safe_send_message(channel, self.str.get('cmd-play-spotify-album-process', 'Processing album `{0}` (`{1}`)').format(res['name'], song_url))
  1301. for i in res['tracks']['items']:
  1302. song_url = i['name'] + ' ' + i['artists'][0]['name']
  1303. log.debug('Processing {0}'.format(song_url))
  1304. await self.cmd_play(message, player, channel, author, permissions, leftover_args, song_url)
  1305. await self.safe_delete_message(procmesg)
  1306. return Response(self.str.get('cmd-play-spotify-album-queued', "Enqueued `{0}` with **{1}** songs.").format(res['name'], len(res['tracks']['items'])))
  1307. elif 'playlist' in parts:
  1308. res = await self.spotify.get_playlist(parts[-3], parts[-1])
  1309. while int(res["tracks"]["total"]) > len(res['tracks']['items']):
  1310. resp = await self.spotify.get_playlist(parts[-3], parts[-1], offset=len(res['tracks']['items']))
  1311. res['tracks']['items'].extend(resp['tracks']['items'])
  1312. await self._do_playlist_checks(permissions, player, author, res['tracks']['items'])
  1313. procmesg = await self.safe_send_message(channel, self.str.get('cmd-play-spotify-playlist-process', 'Processing playlist `{0}` (`{1}`)').format(res['name'], song_url))
  1314. for i in res['tracks']['items']:
  1315. song_url = i['track']['name'] + ' ' + i['track']['artists'][0]['name']
  1316. log.debug('Processing {0}'.format(song_url))
  1317. await self.cmd_play(message, player, channel, author, permissions, leftover_args, song_url)
  1318. await self.safe_delete_message(procmesg)
  1319. return Response(self.str.get('cmd-play-spotify-playlist-queued', "Enqueued `{0}` with **{1}** songs.").format(res['name'], len(res['tracks']['items'])))
  1320. else:
  1321. raise exceptions.CommandError(self.str.get('cmd-play-spotify-unsupported', 'That is not a supported Spotify URI.'), expire_in=30)
  1322. except exceptions.SpotifyError:
  1323. raise exceptions.CommandError(self.str.get('cmd-play-spotify-invalid', 'You either provided an invalid URI, or there was a problem.'))
  1324.  
  1325. async with self.aiolocks[_func_() + ':' + str(author.id)]:
  1326. if permissions.max_songs and player.playlist.count_for_user(author) >= permissions.max_songs:
  1327. raise exceptions.PermissionsError(
  1328. self.str.get('cmd-play-limit', "You have reached your enqueued song limit ({0})").format(permissions.max_songs), expire_in=30
  1329. )
  1330.  
  1331. if player.karaoke_mode and not permissions.bypass_karaoke_mode:
  1332. raise exceptions.PermissionsError(
  1333. self.str.get('karaoke-enabled', "Karaoke mode is enabled, please try again when its disabled!"), expire_in=30
  1334. )
  1335.  
  1336. try:
  1337. info = await self.downloader.extract_info(player.playlist.loop, song_url, download=False, process=False)
  1338. except Exception as e:
  1339. if 'unknown url type' in str(e):
  1340. song_url = song_url.replace(':', '') # it's probably not actually an extractor
  1341. info = await self.downloader.extract_info(player.playlist.loop, song_url, download=False, process=False)
  1342. else:
  1343. raise exceptions.CommandError(e, expire_in=30)
  1344.  
  1345. if not info:
  1346. raise exceptions.CommandError(
  1347. self.str.get('cmd-play-noinfo', "That video cannot be played. Try using the {0}stream command.").format(self.config.command_prefix),
  1348. expire_in=30
  1349. )
  1350.  
  1351. log.debug(info)
  1352.  
  1353. if info.get('extractor', '') not in permissions.extractors and permissions.extractors:
  1354. raise exceptions.PermissionsError(
  1355. self.str.get('cmd-play-badextractor', "You do not have permission to play media from this service."), expire_in=30
  1356. )
  1357.  
  1358. # abstract the search handling away from the user
  1359. # our ytdl options allow us to use search strings as input urls
  1360. if info.get('url', '').startswith('ytsearch'):
  1361. # print("[Command:play] Searching for \"%s\"" % song_url)
  1362. info = await self.downloader.extract_info(
  1363. player.playlist.loop,
  1364. song_url,
  1365. download=False,
  1366. process=True, # ASYNC LAMBDAS WHEN
  1367. on_error=lambda e: asyncio.ensure_future(
  1368. self.safe_send_message(channel, "```\n%s\n```" % e, expire_in=120), loop=self.loop),
  1369. retry_on_error=True
  1370. )
  1371.  
  1372. if not info:
  1373. raise exceptions.CommandError(
  1374. self.str.get('cmd-play-nodata', "Error extracting info from search string, youtubedl returned no data. "
  1375. "You may need to restart the bot if this continues to happen."), expire_in=30
  1376. )
  1377.  
  1378. if not all(info.get('entries', [])):
  1379. # empty list, no data
  1380. log.debug("Got empty list, no data")
  1381. return
  1382.  
  1383. # TODO: handle 'webpage_url' being 'ytsearch:...' or extractor type
  1384. song_url = info['entries'][0]['webpage_url']
  1385. info = await self.downloader.extract_info(player.playlist.loop, song_url, download=False, process=False)
  1386. # Now I could just do: return await self.cmd_play(player, channel, author, song_url)
  1387. # But this is probably fine
  1388.  
  1389. # TODO: Possibly add another check here to see about things like the bandcamp issue
  1390. # TODO: Where ytdl gets the generic extractor version with no processing, but finds two different urls
  1391.  
  1392. if 'entries' in info:
  1393. await self._do_playlist_checks(permissions, player, author, info['entries'])
  1394.  
  1395. num_songs = sum(1 for _ in info['entries'])
  1396.  
  1397. if info['extractor'].lower() in ['youtube:playlist', 'soundcloud:set', 'bandcamp:album']:
  1398. try:
  1399. return await self._cmd_play_playlist_async(player, channel, author, permissions, song_url, info['extractor'])
  1400. except exceptions.CommandError:
  1401. raise
  1402. except Exception as e:
  1403. log.error("Error queuing playlist", exc_info=True)
  1404. raise exceptions.CommandError(self.str.get('cmd-play-playlist-error', "Error queuing playlist:\n`{0}`").format(e), expire_in=30)
  1405.  
  1406. t0 = time.time()
  1407.  
  1408. # My test was 1.2 seconds per song, but we maybe should fudge it a bit, unless we can
  1409. # monitor it and edit the message with the estimated time, but that's some ADVANCED SHIT
  1410. # I don't think we can hook into it anyways, so this will have to do.
  1411. # It would probably be a thread to check a few playlists and get the speed from that
  1412. # Different playlists might download at different speeds though
  1413. wait_per_song = 1.2
  1414.  
  1415. procmesg = await self.safe_send_message(
  1416. channel,
  1417. self.str.get('cmd-play-playlist-gathering-1', 'Gathering playlist information for {0} songs{1}').format(
  1418. num_songs,
  1419. self.str.get('cmd-play-playlist-gathering-2', ', ETA: {0} seconds').format(fixg(
  1420. num_songs * wait_per_song)) if num_songs >= 10 else '.'))
  1421.  
  1422. # We don't have a pretty way of doing this yet. We need either a loop
  1423. # that sends these every 10 seconds or a nice context manager.
  1424. await self.send_typing(channel)
  1425.  
  1426. # TODO: I can create an event emitter object instead, add event functions, and every play list might be asyncified
  1427. # Also have a "verify_entry" hook with the entry as an arg and returns the entry if its ok
  1428.  
  1429. entry_list, position = await player.playlist.import_from(song_url, channel=channel, author=author)
  1430.  
  1431. tnow = time.time()
  1432. ttime = tnow - t0
  1433. listlen = len(entry_list)
  1434. drop_count = 0
  1435.  
  1436. if permissions.max_song_length:
  1437. for e in entry_list.copy():
  1438. if e.duration > permissions.max_song_length:
  1439. player.playlist.entries.remove(e)
  1440. entry_list.remove(e)
  1441. drop_count += 1
  1442. # Im pretty sure there's no situation where this would ever break
  1443. # Unless the first entry starts being played, which would make this a race condition
  1444. if drop_count:
  1445. print("Dropped %s songs" % drop_count)
  1446.  
  1447. log.info("Processed {} songs in {} seconds at {:.2f}s/song, {:+.2g}/song from expected ({}s)".format(
  1448. listlen,
  1449. fixg(ttime),
  1450. ttime / listlen if listlen else 0,
  1451. ttime / listlen - wait_per_song if listlen - wait_per_song else 0,
  1452. fixg(wait_per_song * num_songs))
  1453. )
  1454.  
  1455. await self.safe_delete_message(procmesg)
  1456.  
  1457. if not listlen - drop_count:
  1458. raise exceptions.CommandError(
  1459. self.str.get('cmd-play-playlist-maxduration', "No songs were added, all songs were over max duration (%ss)") % permissions.max_song_length,
  1460. expire_in=30
  1461. )
  1462.  
  1463. reply_text = self.str.get('cmd-play-playlist-reply', "Enqueued **%s** songs to be played. Position in queue: %s")
  1464. btext = str(listlen - drop_count)
  1465.  
  1466. else:
  1467. if info.get('extractor', '').startswith('youtube:playlist'):
  1468. try:
  1469. info = await self.downloader.extract_info(player.playlist.loop, 'https://www.youtube.com/watch?v=%s' % info.get('url', ''), download=False, process=False)
  1470. except Exception as e:
  1471. raise exceptions.CommandError(e, expire_in=30)
  1472.  
  1473. if permissions.max_song_length and info.get('duration', 0) > permissions.max_song_length:
  1474. raise exceptions.PermissionsError(
  1475. self.str.get('cmd-play-song-limit', "Song duration exceeds limit ({0} > {1})").format(info['duration'], permissions.max_song_length),
  1476. expire_in=30
  1477. )
  1478.  
  1479. try:
  1480. entry, position = await player.playlist.add_entry(song_url, channel=channel, author=author)
  1481.  
  1482. except exceptions.WrongEntryTypeError as e:
  1483. if e.use_url == song_url:
  1484. log.warning("Determined incorrect entry type, but suggested url is the same. Help.")
  1485.  
  1486. log.debug("Assumed url \"%s\" was a single entry, was actually a playlist" % song_url)
  1487. log.debug("Using \"%s\" instead" % e.use_url)
  1488.  
  1489. return await self.cmd_play(player, channel, author, permissions, leftover_args, e.use_url)
  1490.  
  1491. reply_text = self.str.get('cmd-play-song-reply', "Enqueued `%s` to be played. Position in queue: %s")
  1492. btext = entry.title
  1493.  
  1494.  
  1495. if position == 1 and player.is_stopped:
  1496. position = self.str.get('cmd-play-next', 'Up next!')
  1497. reply_text %= (btext, position)
  1498.  
  1499. else:
  1500. try:
  1501. time_until = await player.playlist.estimate_time_until(position, player)
  1502. reply_text += self.str.get('cmd-play-eta', ' - estimated time until playing: %s')
  1503. except:
  1504. traceback.print_exc()
  1505. time_until = ''
  1506.  
  1507. reply_text %= (btext, position, ftimedelta(time_until))
  1508.  
  1509. return Response(reply_text, delete_after=30)
  1510.  
  1511. async def _cmd_play_playlist_async(self, player, channel, author, permissions, playlist_url, extractor_type):
  1512. """
  1513. Secret handler to use the async wizardry to make playlist queuing non-"blocking"
  1514. """
  1515.  
  1516. await self.send_typing(channel)
  1517. info = await self.downloader.extract_info(player.playlist.loop, playlist_url, download=False, process=False)
  1518.  
  1519. if not info:
  1520. raise exceptions.CommandError(self.str.get('cmd-play-playlist-invalid', "That playlist cannot be played."))
  1521.  
  1522. num_songs = sum(1 for _ in info['entries'])
  1523. t0 = time.time()
  1524.  
  1525. busymsg = await self.safe_send_message(
  1526. channel, self.str.get('cmd-play-playlist-process', "Processing {0} songs...").format(num_songs)) # TODO: From playlist_title
  1527. await self.send_typing(channel)
  1528.  
  1529. entries_added = 0
  1530. if extractor_type == 'youtube:playlist':
  1531. try:
  1532. entries_added = await player.playlist.async_process_youtube_playlist(
  1533. playlist_url, channel=channel, author=author)
  1534. # TODO: Add hook to be called after each song
  1535. # TODO: Add permissions
  1536.  
  1537. except Exception:
  1538. log.error("Error processing playlist", exc_info=True)
  1539. raise exceptions.CommandError(self.str.get('cmd-play-playlist-queueerror', 'Error handling playlist {0} queuing.').format(playlist_url), expire_in=30)
  1540.  
  1541. elif extractor_type.lower() in ['soundcloud:set', 'bandcamp:album']:
  1542. try:
  1543. entries_added = await player.playlist.async_process_sc_bc_playlist(
  1544. playlist_url, channel=channel, author=author)
  1545. # TODO: Add hook to be called after each song
  1546. # TODO: Add permissions
  1547.  
  1548. except Exception:
  1549. log.error("Error processing playlist", exc_info=True)
  1550. raise exceptions.CommandError(self.str.get('cmd-play-playlist-queueerror', 'Error handling playlist {0} queuing.').format(playlist_url), expire_in=30)
  1551.  
  1552.  
  1553. songs_processed = len(entries_added)
  1554. drop_count = 0
  1555. skipped = False
  1556.  
  1557. if permissions.max_song_length:
  1558. for e in entries_added.copy():
  1559. if e.duration > permissions.max_song_length:
  1560. try:
  1561. player.playlist.entries.remove(e)
  1562. entries_added.remove(e)
  1563. drop_count += 1
  1564. except:
  1565. pass
  1566.  
  1567. if drop_count:
  1568. log.debug("Dropped %s songs" % drop_count)
  1569.  
  1570. if player.current_entry and player.current_entry.duration > permissions.max_song_length:
  1571. await self.safe_delete_message(self.server_specific_data[channel.guild]['last_np_msg'])
  1572. self.server_specific_data[channel.guild]['last_np_msg'] = None
  1573. skipped = True
  1574. player.skip()
  1575. entries_added.pop()
  1576.  
  1577. await self.safe_delete_message(busymsg)
  1578.  
  1579. songs_added = len(entries_added)
  1580. tnow = time.time()
  1581. ttime = tnow - t0
  1582. wait_per_song = 1.2
  1583. # TODO: actually calculate wait per song in the process function and return that too
  1584.  
  1585. # This is technically inaccurate since bad songs are ignored but still take up time
  1586. log.info("Processed {}/{} songs in {} seconds at {:.2f}s/song, {:+.2g}/song from expected ({}s)".format(
  1587. songs_processed,
  1588. num_songs,
  1589. fixg(ttime),
  1590. ttime / num_songs if num_songs else 0,
  1591. ttime / num_songs - wait_per_song if num_songs - wait_per_song else 0,
  1592. fixg(wait_per_song * num_songs))
  1593. )
  1594.  
  1595. if not songs_added:
  1596. basetext = self.str.get('cmd-play-playlist-maxduration', "No songs were added, all songs were over max duration (%ss)") % permissions.max_song_length
  1597. if skipped:
  1598. basetext += self.str.get('cmd-play-playlist-skipped', "\nAdditionally, the current song was skipped for being too long.")
  1599.  
  1600. raise exceptions.CommandError(basetext, expire_in=30)
  1601.  
  1602. return Response(self.str.get('cmd-play-playlist-reply-secs', "Enqueued {0} songs to be played in {1} seconds").format(
  1603. songs_added, fixg(ttime, 1)), delete_after=30)
  1604.  
  1605. async def cmd_stream(self, player, channel, author, permissions, song_url):
  1606. """
  1607. Usage:
  1608. {command_prefix}stream song_link
  1609.  
  1610. Enqueue a media stream.
  1611. This could mean an actual stream like Twitch or shoutcast, or simply streaming
  1612. media without predownloading it. Note: FFmpeg is notoriously bad at handling
  1613. streams, especially on poor connections. You have been warned.
  1614. """
  1615.  
  1616. song_url = song_url.strip('<>')
  1617.  
  1618. if permissions.max_songs and player.playlist.count_for_user(author) >= permissions.max_songs:
  1619. raise exceptions.PermissionsError(
  1620. self.str.get('cmd-stream-limit', "You have reached your enqueued song limit ({0})").format(permissions.max_songs), expire_in=30
  1621. )
  1622.  
  1623. if player.karaoke_mode and not permissions.bypass_karaoke_mode:
  1624. raise exceptions.PermissionsError(
  1625. self.str.get('karaoke-enabled', "Karaoke mode is enabled, please try again when its disabled!"), expire_in=30
  1626. )
  1627.  
  1628. await self.send_typing(channel)
  1629. await player.playlist.add_stream_entry(song_url, channel=channel, author=author)
  1630.  
  1631. return Response(self.str.get('cmd-stream-success', "Streaming."), delete_after=6)
  1632.  
  1633. async def cmd_search(self, message, player, channel, author, permissions, leftover_args):
  1634. """
  1635. Usage:
  1636. {command_prefix}search [service] [number] query
  1637.  
  1638. Searches a service for a video and adds it to the queue.
  1639. - service: any one of the following services:
  1640. - youtube (yt) (default if unspecified)
  1641. - soundcloud (sc)
  1642. - yahoo (yh)
  1643. - number: return a number of video results and waits for user to choose one
  1644. - defaults to 3 if unspecified
  1645. - note: If your search query starts with a number,
  1646. you must put your query in quotes
  1647. - ex: {command_prefix}search 2 "I ran seagulls"
  1648. The command issuer can use reactions to indicate their response to each result.
  1649. """
  1650.  
  1651. if permissions.max_songs and player.playlist.count_for_user(author) > permissions.max_songs:
  1652. raise exceptions.PermissionsError(
  1653. self.str.get('cmd-search-limit', "You have reached your playlist item limit ({0})").format(permissions.max_songs),
  1654. expire_in=30
  1655. )
  1656.  
  1657. if player.karaoke_mode and not permissions.bypass_karaoke_mode:
  1658. raise exceptions.PermissionsError(
  1659. self.str.get('karaoke-enabled', "Karaoke mode is enabled, please try again when its disabled!"), expire_in=30
  1660. )
  1661.  
  1662. def argcheck():
  1663. if not leftover_args:
  1664. # noinspection PyUnresolvedReferences
  1665. raise exceptions.CommandError(
  1666. self.str.get('cmd-search-noquery', "Please specify a search query.\n%s") % dedent(
  1667. self.cmd_search.__doc__.format(command_prefix=self.config.command_prefix)),
  1668. expire_in=60
  1669. )
  1670.  
  1671. argcheck()
  1672.  
  1673. try:
  1674. leftover_args = shlex.split(' '.join(leftover_args))
  1675. except ValueError:
  1676. raise exceptions.CommandError(self.str.get('cmd-search-noquote', "Please quote your search query properly."), expire_in=30)
  1677.  
  1678. service = 'youtube'
  1679. items_requested = 3
  1680. max_items = permissions.max_search_items
  1681. services = {
  1682. 'youtube': 'ytsearch',
  1683. 'soundcloud': 'scsearch',
  1684. 'yahoo': 'yvsearch',
  1685. 'yt': 'ytsearch',
  1686. 'sc': 'scsearch',
  1687. 'yh': 'yvsearch'
  1688. }
  1689.  
  1690. if leftover_args[0] in services:
  1691. service = leftover_args.pop(0)
  1692. argcheck()
  1693.  
  1694. if leftover_args[0].isdigit():
  1695. items_requested = int(leftover_args.pop(0))
  1696. argcheck()
  1697.  
  1698. if items_requested > max_items:
  1699. raise exceptions.CommandError(self.str.get('cmd-search-searchlimit', "You cannot search for more than %s videos") % max_items)
  1700.  
  1701. # Look jake, if you see this and go "what the fuck are you doing"
  1702. # and have a better idea on how to do this, i'd be delighted to know.
  1703. # I don't want to just do ' '.join(leftover_args).strip("\"'")
  1704. # Because that eats both quotes if they're there
  1705. # where I only want to eat the outermost ones
  1706. if leftover_args[0][0] in '\'"':
  1707. lchar = leftover_args[0][0]
  1708. leftover_args[0] = leftover_args[0].lstrip(lchar)
  1709. leftover_args[-1] = leftover_args[-1].rstrip(lchar)
  1710.  
  1711. search_query = '%s%s:%s' % (services[service], items_requested, ' '.join(leftover_args))
  1712.  
  1713. search_msg = await self.safe_send_message(channel, self.str.get('cmd-search-searching', "Searching for videos..."))
  1714. await self.send_typing(channel)
  1715.  
  1716. try:
  1717. info = await self.downloader.extract_info(player.playlist.loop, search_query, download=False, process=True)
  1718.  
  1719. except Exception as e:
  1720. await self.safe_edit_message(search_msg, str(e), send_if_fail=True)
  1721. return
  1722. else:
  1723. await self.safe_delete_message(search_msg)
  1724.  
  1725. if not info:
  1726. return Response(self.str.get('cmd-search-none', "No videos found."), delete_after=30)
  1727.  
  1728. for e in info['entries']:
  1729. result_message = await self.safe_send_message(channel, self.str.get('cmd-search-result', "Result {0}/{1}: {2}").format(
  1730. info['entries'].index(e) + 1, len(info['entries']), e['webpage_url']))
  1731.  
  1732. def check(reaction, user):
  1733. return user == message.author and reaction.message.id == result_message.id # why can't these objs be compared directly?
  1734.  
  1735. reactions = ['\u2705', '\U0001F6AB', '\U0001F3C1']
  1736. for r in reactions:
  1737. await result_message.add_reaction(r)
  1738.  
  1739. try:
  1740. reaction, user = await self.wait_for('reaction_add', timeout=30.0, check=check)
  1741. except asyncio.TimeoutError:
  1742. await self.safe_delete_message(result_message)
  1743. return
  1744.  
  1745. if str(reaction.emoji) == '\u2705': # check
  1746. await self.safe_delete_message(result_message)
  1747. await self.cmd_play(message, player, channel, author, permissions, [], e['webpage_url'])
  1748. return Response(self.str.get('cmd-search-accept', "Alright, coming right up!"), delete_after=30)
  1749. elif str(reaction.emoji) == '\U0001F6AB': # cross
  1750. await self.safe_delete_message(result_message)
  1751. continue
  1752. else:
  1753. await self.safe_delete_message(result_message)
  1754. break
  1755.  
  1756. return Response(self.str.get('cmd-search-decline', "Oh well :("), delete_after=30)
  1757.  
  1758. async def cmd_np(self, player, channel, guild, message):
  1759. """
  1760. Usage:
  1761. {command_prefix}np
  1762.  
  1763. Displays the current song in chat.
  1764. """
  1765.  
  1766. if player.current_entry:
  1767. if self.server_specific_data[guild]['last_np_msg']:
  1768. await self.safe_delete_message(self.server_specific_data[guild]['last_np_msg'])
  1769. self.server_specific_data[guild]['last_np_msg'] = None
  1770.  
  1771. # TODO: Fix timedelta garbage with util function
  1772. song_progress = ftimedelta(timedelta(seconds=player.progress))
  1773. song_total = ftimedelta(timedelta(seconds=player.current_entry.duration))
  1774.  
  1775. streaming = isinstance(player.current_entry, StreamPlaylistEntry)
  1776. prog_str = ('`[{progress}]`' if streaming else '`[{progress}/{total}]`').format(
  1777. progress=song_progress, total=song_total
  1778. )
  1779. prog_bar_str = ''
  1780.  
  1781. # percentage shows how much of the current song has already been played
  1782. percentage = 0.0
  1783. if player.current_entry.duration > 0:
  1784. percentage = player.progress / player.current_entry.duration
  1785.  
  1786. # create the actual bar
  1787. progress_bar_length = 30
  1788. for i in range(progress_bar_length):
  1789. if (percentage < 1 / progress_bar_length * i):
  1790. prog_bar_str += '□'
  1791. else:
  1792. prog_bar_str += '■'
  1793.  
  1794. action_text = self.str.get('cmd-np-action-streaming', 'Streaming') if streaming else self.str.get('cmd-np-action-playing', 'Playing')
  1795.  
  1796. if player.current_entry.meta.get('channel', False) and player.current_entry.meta.get('author', False):
  1797. np_text = self.str.get('cmd-np-reply-author', "Now {action}: **{title}** added by **{author}**\nProgress: {progress_bar} {progress}\n\N{WHITE RIGHT POINTING BACKHAND INDEX} <{url}>").format(
  1798. action=action_text,
  1799. title=player.current_entry.title,
  1800. author=player.current_entry.meta['author'].name,
  1801. progress_bar=prog_bar_str,
  1802. progress=prog_str,
  1803. url=player.current_entry.url
  1804. )
  1805. else:
  1806.  
  1807. np_text = self.str.get('cmd-np-reply-noauthor', "Now {action}: **{title}**\nProgress: {progress_bar} {progress}\n\N{WHITE RIGHT POINTING BACKHAND INDEX} <{url}>").format(
  1808.  
  1809. action=action_text,
  1810. title=player.current_entry.title,
  1811. progress_bar=prog_bar_str,
  1812. progress=prog_str,
  1813. url=player.current_entry.url
  1814. )
  1815.  
  1816. self.server_specific_data[guild]['last_np_msg'] = await self.safe_send_message(channel, np_text)
  1817. await self._manual_delete_check(message)
  1818. else:
  1819. return Response(
  1820. self.str.get('cmd-np-none', 'There are no songs queued! Queue something with {0}play.') .format(self.config.command_prefix),
  1821. delete_after=30
  1822. )
  1823.  
  1824. async def cmd_summon(self, channel, guild, author, voice_channel):
  1825. """
  1826. Usage:
  1827. {command_prefix}summon
  1828.  
  1829. Call the bot to the summoner's voice channel.
  1830. """
  1831.  
  1832. if not author.voice:
  1833. raise exceptions.CommandError(self.str.get('cmd-summon-novc', 'You are not connected to voice. Try joining a voice channel!'))
  1834.  
  1835. voice_client = self.voice_client_in(guild)
  1836. if voice_client and guild == author.voice.channel.guild:
  1837. await voice_client.move_to(author.voice.channel)
  1838. else:
  1839. # move to _verify_vc_perms?
  1840. chperms = author.voice.channel.permissions_for(guild.me)
  1841.  
  1842. if not chperms.connect:
  1843. log.warning("Cannot join channel '{0}', no permission.".format(author.voice.channel.name))
  1844. raise exceptions.CommandError(
  1845. self.str.get('cmd-summon-noperms-connect', "Cannot join channel `{0}`, no permission to connect.").format(author.voice.channel.name),
  1846. expire_in=25
  1847. )
  1848.  
  1849. elif not chperms.speak:
  1850. log.warning("Cannot join channel '{0}', no permission to speak.".format(author.voice.channel.name))
  1851. raise exceptions.CommandError(
  1852. self.str.get('cmd-summon-noperms-speak', "Cannot join channel `{0}`, no permission to speak.").format(author.voice.channel.name),
  1853. expire_in=25
  1854. )
  1855.  
  1856. player = await self.get_player(author.voice.channel, create=True, deserialize=self.config.persistent_queue)
  1857.  
  1858. if player.is_stopped:
  1859. player.play()
  1860.  
  1861. if self.config.auto_playlist:
  1862. await self.on_player_finished_playing(player)
  1863.  
  1864. log.info("Joining {0.guild.name}/{0.name}".format(author.voice.channel))
  1865.  
  1866. return Response(self.str.get('cmd-summon-reply', 'Connected to `{0.name}`').format(author.voice.channel))
  1867.  
  1868. async def cmd_pause(self, player):
  1869. """
  1870. Usage:
  1871. {command_prefix}pause
  1872.  
  1873. Pauses playback of the current song.
  1874. """
  1875.  
  1876. if player.is_playing:
  1877. player.pause()
  1878. return Response(self.str.get('cmd-pause-reply', 'Paused music in `{0.name}`').format(player.voice_client.channel))
  1879.  
  1880. else:
  1881. raise exceptions.CommandError(self.str.get('cmd-pause-none', 'Player is not playing.'), expire_in=30)
  1882.  
  1883. async def cmd_resume(self, player):
  1884. """
  1885. Usage:
  1886. {command_prefix}resume
  1887.  
  1888. Resumes playback of a paused song.
  1889. """
  1890.  
  1891. if player.is_paused:
  1892. player.resume()
  1893. return Response(self.str.get('cmd-resume-reply', 'Resumed music in `{0.name}`').format(player.voice_client.channel), delete_after=15)
  1894.  
  1895. else:
  1896. raise exceptions.CommandError(self.str.get('cmd-resume-none', 'Player is not paused.'), expire_in=30)
  1897.  
  1898. async def cmd_shuffle(self, channel, player):
  1899. """
  1900. Usage:
  1901. {command_prefix}shuffle
  1902.  
  1903. Shuffles the server's queue.
  1904. """
  1905.  
  1906. player.playlist.shuffle()
  1907.  
  1908. cards = ['\N{BLACK SPADE SUIT}', '\N{BLACK CLUB SUIT}', '\N{BLACK HEART SUIT}', '\N{BLACK DIAMOND SUIT}']
  1909. random.shuffle(cards)
  1910.  
  1911. hand = await self.safe_send_message(channel, ' '.join(cards))
  1912. await asyncio.sleep(0.6)
  1913.  
  1914. for x in range(4):
  1915. random.shuffle(cards)
  1916. await self.safe_edit_message(hand, ' '.join(cards))
  1917. await asyncio.sleep(0.6)
  1918.  
  1919. await self.safe_delete_message(hand, quiet=True)
  1920. return Response(self.str.get('cmd-shuffle-reply', "Shuffled `{0}`'s queue.").format(player.voice_client.channel.guild), delete_after=15)
  1921.  
  1922. async def cmd_clear(self, player, author):
  1923. """
  1924. Usage:
  1925. {command_prefix}clear
  1926.  
  1927. Clears the playlist.
  1928. """
  1929.  
  1930. player.playlist.clear()
  1931. return Response(self.str.get('cmd-clear-reply', "Cleared `{0}`'s queue").format(player.voice_client.channel.guild), delete_after=20)
  1932.  
  1933. async def cmd_remove(self, user_mentions, message, author, permissions, channel, player, index=None):
  1934. """
  1935. Usage:
  1936. {command_prefix}remove [# in queue]
  1937.  
  1938. Removes queued songs. If a number is specified, removes that song in the queue, otherwise removes the most recently queued song.
  1939. """
  1940.  
  1941. if not player.playlist.entries:
  1942. raise exceptions.CommandError(self.str.get('cmd-remove-none', "There's nothing to remove!"), expire_in=20)
  1943.  
  1944. if user_mentions:
  1945. for user in user_mentions:
  1946. if author.id == self.config.owner_id or permissions.remove or author == user:
  1947. try:
  1948. entry_indexes = [e for e in player.playlist.entries if e.meta.get('author', None) == user]
  1949. for entry in entry_indexes:
  1950. player.playlist.entries.remove(entry)
  1951. entry_text = '%s ' % len(entry_indexes) + 'item'
  1952. if len(entry_indexes) > 1:
  1953. entry_text += 's'
  1954. return Response(self.str.get('cmd-remove-reply', "Removed `{0}` added by `{1}`").format(entry_text, user.name).strip())
  1955.  
  1956. except ValueError:
  1957. raise exceptions.CommandError(self.str.get('cmd-remove-missing', "Nothing found in the queue from user `%s`") % user.name, expire_in=20)
  1958.  
  1959. raise exceptions.PermissionsError(
  1960. self.str.get('cmd-remove-noperms', "You do not have the valid permissions to remove that entry from the queue, make sure you're the one who queued it or have instant skip permissions"), expire_in=20)
  1961.  
  1962. if not index:
  1963. index = len(player.playlist.entries)
  1964.  
  1965. try:
  1966. index = int(index)
  1967. except (TypeError, ValueError):
  1968. raise exceptions.CommandError(self.str.get('cmd-remove-invalid', "Invalid number. Use {}queue to find queue positions.").format(self.config.command_prefix), expire_in=20)
  1969.  
  1970. if index > len(player.playlist.entries):
  1971. raise exceptions.CommandError(self.str.get('cmd-remove-invalid', "Invalid number. Use {}queue to find queue positions.").format(self.config.command_prefix), expire_in=20)
  1972.  
  1973. if author.id == self.config.owner_id or permissions.remove or author == player.playlist.get_entry_at_index(index - 1).meta.get('author', None):
  1974. entry = player.playlist.delete_entry_at_index((index - 1))
  1975. await self._manual_delete_check(message)
  1976. if entry.meta.get('channel', False) and entry.meta.get('author', False):
  1977. return Response(self.str.get('cmd-remove-reply-author', "Removed entry `{0}` added by `{1}`").format(entry.title, entry.meta['author'].name).strip())
  1978. else:
  1979. return Response(self.str.get('cmd-remove-reply-noauthor', "Removed entry `{0}`").format(entry.title).strip())
  1980. else:
  1981. raise exceptions.PermissionsError(
  1982. self.str.get('cmd-remove-noperms', "You do not have the valid permissions to remove that entry from the queue, make sure you're the one who queued it or have instant skip permissions"), expire_in=20
  1983. )
  1984.  
  1985. async def cmd_skip(self, player, channel, author, message, permissions, voice_channel, param=''):
  1986. """
  1987. Usage:
  1988. {command_prefix}skip [force/f]
  1989.  
  1990. Skips the current song when enough votes are cast.
  1991. Owners and those with the instaskip permission can add 'force' or 'f' after the command to force skip.
  1992. """
  1993.  
  1994. if player.is_stopped:
  1995. raise exceptions.CommandError(self.str.get('cmd-skip-none', "Can't skip! The player is not playing!"), expire_in=20)
  1996.  
  1997. if not player.current_entry:
  1998. if player.playlist.peek():
  1999. if player.playlist.peek()._is_downloading:
  2000. return Response(self.str.get('cmd-skip-dl', "The next song (`%s`) is downloading, please wait.") % player.playlist.peek().title)
  2001.  
  2002. elif player.playlist.peek().is_downloaded:
  2003. print("The next song will be played shortly. Please wait.")
  2004. else:
  2005. print("Something odd is happening. "
  2006. "You might want to restart the bot if it doesn't start working.")
  2007. else:
  2008. print("Something strange is happening. "
  2009. "You might want to restart the bot if it doesn't start working.")
  2010.  
  2011. current_entry = player.current_entry
  2012.  
  2013. if (param.lower() in ['force', 'f']) or self.config.legacy_skip:
  2014. if author.id == self.config.owner_id \
  2015. or permissions.instaskip \
  2016. or (self.config.allow_author_skip and author == player.current_entry.meta.get('author', None)):
  2017.  
  2018. player.skip() # TODO: check autopause stuff here
  2019. await self._manual_delete_check(message)
  2020. return Response(self.str.get('cmd-skip-force', 'Force skipped `{}`.').format(current_entry.title), reply=True, delete_after=30)
  2021. else:
  2022. raise exceptions.PermissionsError(self.str.get('cmd-skip-force-noperms', 'You do not have permission to force skip.'), expire_in=30)
  2023.  
  2024. # TODO: ignore person if they're deaf or take them out of the list or something?
  2025. # Currently is recounted if they vote, deafen, then vote
  2026.  
  2027. num_voice = sum(1 for m in voice_channel.members if not (
  2028. m.voice.deaf or m.voice.self_deaf or m == self.user))
  2029. if num_voice == 0: num_voice = 1 # incase all users are deafened, to avoid divison by zero
  2030.  
  2031. num_skips = player.skip_state.add_skipper(author.id, message)
  2032.  
  2033. skips_remaining = min(
  2034. self.config.skips_required,
  2035. math.ceil(self.config.skip_ratio_required / (1 / num_voice)) # Number of skips from config ratio
  2036. ) - num_skips
  2037.  
  2038. if skips_remaining <= 0:
  2039. player.skip() # check autopause stuff here
  2040. return Response(
  2041. self.str.get('cmd-skip-reply-skipped-1', 'Your skip for `{0}` was acknowledged.\nThe vote to skip has been passed.{1}').format(
  2042. current_entry.title,
  2043. self.str.get('cmd-skip-reply-skipped-2', ' Next song coming up!') if player.playlist.peek() else ''
  2044. ),
  2045. reply=True,
  2046. delete_after=20
  2047. )
  2048.  
  2049. else:
  2050. # TODO: When a song gets skipped, delete the old x needed to skip messages
  2051. return Response(
  2052. self.str.get('cmd-skip-reply-voted-1', 'Your skip for `{0}` was acknowledged.\n**{1}** more {2} required to vote to skip this song.').format(
  2053. current_entry.title,
  2054. skips_remaining,
  2055. self.str.get('cmd-skip-reply-voted-2', 'person is') if skips_remaining == 1 else self.str.get('cmd-skip-reply-voted-3', 'people are')
  2056. ),
  2057. reply=True,
  2058. delete_after=20
  2059. )
  2060.  
  2061. async def cmd_volume(self, message, player, new_volume=None):
  2062. """
  2063. Usage:
  2064. {command_prefix}volume (+/-)[volume]
  2065.  
  2066. Sets the playback volume. Accepted values are from 1 to 100.
  2067. Putting + or - before the volume will make the volume change relative to the current volume.
  2068. """
  2069.  
  2070. if not new_volume:
  2071. return Response(self.str.get('cmd-volume-current', 'Current volume: `%s%%`') % int(player.volume * 100), reply=True, delete_after=20)
  2072.  
  2073. relative = False
  2074. if new_volume[0] in '+-':
  2075. relative = True
  2076.  
  2077. try:
  2078. new_volume = int(new_volume)
  2079.  
  2080. except ValueError:
  2081. raise exceptions.CommandError(self.str.get('cmd-volume-invalid', '`{0}` is not a valid number').format(new_volume), expire_in=20)
  2082.  
  2083. vol_change = None
  2084. if relative:
  2085. vol_change = new_volume
  2086. new_volume += (player.volume * 100)
  2087.  
  2088. old_volume = int(player.volume * 100)
  2089.  
  2090. if 0 < new_volume <= 100:
  2091. player.volume = new_volume / 100.0
  2092.  
  2093. return Response(self.str.get('cmd-volume-reply', 'Updated volume from **%d** to **%d**') % (old_volume, new_volume), reply=True, delete_after=20)
  2094.  
  2095. else:
  2096. if relative:
  2097. raise exceptions.CommandError(
  2098. self.str.get('cmd-volume-unreasonable-relative', 'Unreasonable volume change provided: {}{:+} -> {}%. Provide a change between {} and {:+}.').format(
  2099. old_volume, vol_change, old_volume + vol_change, 1 - old_volume, 100 - old_volume), expire_in=20)
  2100. else:
  2101. raise exceptions.CommandError(
  2102. self.str.get('cmd-volume-unreasonable-absolute', 'Unreasonable volume provided: {}%. Provide a value between 1 and 100.').format(new_volume), expire_in=20)
  2103.  
  2104. @owner_only
  2105. async def cmd_option(self, player, option, value):
  2106. """
  2107. Usage:
  2108. {command_prefix}option [option] [on/y/enabled/off/n/disabled]
  2109.  
  2110. Changes a config option without restarting the bot. Changes aren't permanent and
  2111. only last until the bot is restarted. To make permanent changes, edit the
  2112. config file.
  2113.  
  2114. Valid options:
  2115. autoplaylist, save_videos, now_playing_mentions, auto_playlist_random, auto_pause,
  2116. delete_messages, delete_invoking, write_current_song
  2117.  
  2118. For information about these options, see the option's comment in the config file.
  2119. """
  2120.  
  2121. option = option.lower()
  2122. value = value.lower()
  2123. bool_y = ['on', 'y', 'enabled']
  2124. bool_n = ['off', 'n', 'disabled']
  2125. generic = ['save_videos', 'now_playing_mentions', 'auto_playlist_random',
  2126. 'auto_pause', 'delete_messages', 'delete_invoking',
  2127. 'write_current_song'] # these need to match attribute names in the Config class
  2128. if option in ['autoplaylist', 'auto_playlist']:
  2129. if value in bool_y:
  2130. if self.config.auto_playlist:
  2131. raise exceptions.CommandError(self.str.get('cmd-option-autoplaylist-enabled', 'The autoplaylist is already enabled!'))
  2132. else:
  2133. if not self.autoplaylist:
  2134. raise exceptions.CommandError(self.str.get('cmd-option-autoplaylist-none', 'There are no entries in the autoplaylist file.'))
  2135. self.config.auto_playlist = True
  2136. await self.on_player_finished_playing(player)
  2137. elif value in bool_n:
  2138. if not self.config.auto_playlist:
  2139. raise exceptions.CommandError(self.str.get('cmd-option-autoplaylist-disabled', 'The autoplaylist is already disabled!'))
  2140. else:
  2141. self.config.auto_playlist = False
  2142. else:
  2143. raise exceptions.CommandError(self.str.get('cmd-option-invalid-value', 'The value provided was not valid.'))
  2144. return Response("The autoplaylist is now " + ['disabled', 'enabled'][self.config.auto_playlist] + '.')
  2145. else:
  2146. is_generic = [o for o in generic if o == option] # check if it is a generic bool option
  2147. if is_generic and (value in bool_y or value in bool_n):
  2148. name = is_generic[0]
  2149. log.debug('Setting attribute {0}'.format(name))
  2150. setattr(self.config, name, True if value in bool_y else False) # this is scary but should work
  2151. attr = getattr(self.config, name)
  2152. res = "The option {0} is now ".format(option) + ['disabled', 'enabled'][attr] + '.'
  2153. log.warning('Option overriden for this session: {0}'.format(res))
  2154. return Response(res)
  2155. else:
  2156. raise exceptions.CommandError(self.str.get('cmd-option-invalid-param' ,'The parameters provided were invalid.'))
  2157.  
  2158. async def cmd_queue(self, channel, player):
  2159. """
  2160. Usage:
  2161. {command_prefix}queue
  2162.  
  2163. Prints the current song queue.
  2164. """
  2165.  
  2166. lines = []
  2167. unlisted = 0
  2168. andmoretext = '* ... and %s more*' % ('x' * len(player.playlist.entries))
  2169.  
  2170. if player.is_playing:
  2171. # TODO: Fix timedelta garbage with util function
  2172. song_progress = ftimedelta(timedelta(seconds=player.progress))
  2173. song_total = ftimedelta(timedelta(seconds=player.current_entry.duration))
  2174. prog_str = '`[%s/%s]`' % (song_progress, song_total)
  2175.  
  2176. if player.current_entry.meta.get('channel', False) and player.current_entry.meta.get('author', False):
  2177. lines.append(self.str.get('cmd-queue-playing-author', "Currently playing: `{0}` added by `{1}` {2}\n").format(
  2178. player.current_entry.title, player.current_entry.meta['author'].name, prog_str))
  2179. else:
  2180. lines.append(self.str.get('cmd-queue-playing-noauthor', "Currently playing: `{0}` {1}\n").format(player.current_entry.title, prog_str))
  2181.  
  2182.  
  2183. for i, item in enumerate(player.playlist, 1):
  2184. if item.meta.get('channel', False) and item.meta.get('author', False):
  2185. nextline = self.str.get('cmd-queue-entry-author', '{0} -- `{1}` by `{2}`').format(i, item.title, item.meta['author'].name).strip()
  2186. else:
  2187. nextline = self.str.get('cmd-queue-entry-noauthor', '{0} -- `{1}`').format(i, item.title).strip()
  2188.  
  2189. currentlinesum = sum(len(x) + 1 for x in lines) # +1 is for newline char
  2190.  
  2191. if (currentlinesum + len(nextline) + len(andmoretext) > DISCORD_MSG_CHAR_LIMIT) or (i > self.config.queue_length):
  2192. if currentlinesum + len(andmoretext):
  2193. unlisted += 1
  2194. continue
  2195.  
  2196. lines.append(nextline)
  2197.  
  2198. if unlisted:
  2199. lines.append(self.str.get('cmd-queue-more', '\n... and %s more') % unlisted)
  2200.  
  2201. if not lines:
  2202. lines.append(
  2203. self.str.get('cmd-queue-none', 'There are no songs queued! Queue something with {}play.').format(self.config.command_prefix))
  2204.  
  2205. message = '\n'.join(lines)
  2206. return Response(message, delete_after=30)
  2207.  
  2208. async def cmd_clean(self, message, channel, guild, author, search_range=50):
  2209. """
  2210. Usage:
  2211. {command_prefix}clean [range]
  2212.  
  2213. Removes up to [range] messages the bot has posted in chat. Default: 50, Max: 1000
  2214. """
  2215.  
  2216. try:
  2217. float(search_range) # lazy check
  2218. search_range = min(int(search_range), 1000)
  2219. except:
  2220. return Response(self.str.get('cmd-clean-invalid', "Invalid parameter. Please provide a number of messages to search."), reply=True, delete_after=8)
  2221.  
  2222. await self.safe_delete_message(message, quiet=True)
  2223.  
  2224. def is_possible_command_invoke(entry):
  2225. valid_call = any(
  2226. entry.content.startswith(prefix) for prefix in [self.config.command_prefix]) # can be expanded
  2227. return valid_call and not entry.content[1:2].isspace()
  2228.  
  2229. delete_invokes = True
  2230. delete_all = channel.permissions_for(author).manage_messages or self.config.owner_id == author.id
  2231.  
  2232. def check(message):
  2233. if is_possible_command_invoke(message) and delete_invokes:
  2234. return delete_all or message.author == author
  2235. return message.author == self.user
  2236.  
  2237. if self.user.bot:
  2238. if channel.permissions_for(guild.me).manage_messages:
  2239. deleted = await channel.purge(check=check, limit=search_range, before=message)
  2240. return Response(self.str.get('cmd-clean-reply', 'Cleaned up {0} message{1}.').format(len(deleted), 's' * bool(deleted)), delete_after=15)
  2241.  
  2242. async def cmd_pldump(self, channel, author, song_url):
  2243. """
  2244. Usage:
  2245. {command_prefix}pldump url
  2246.  
  2247. Dumps the individual urls of a playlist
  2248. """
  2249.  
  2250. try:
  2251. info = await self.downloader.extract_info(self.loop, song_url.strip('<>'), download=False, process=False)
  2252. except Exception as e:
  2253. raise exceptions.CommandError("Could not extract info from input url\n%s\n" % e, expire_in=25)
  2254.  
  2255. if not info:
  2256. raise exceptions.CommandError("Could not extract info from input url, no data.", expire_in=25)
  2257.  
  2258. if not info.get('entries', None):
  2259. # TODO: Retarded playlist checking
  2260. # set(url, webpageurl).difference(set(url))
  2261.  
  2262. if info.get('url', None) != info.get('webpage_url', info.get('url', None)):
  2263. raise exceptions.CommandError("This does not seem to be a playlist.", expire_in=25)
  2264. else:
  2265. return await self.cmd_pldump(channel, info.get(''))
  2266.  
  2267. linegens = defaultdict(lambda: None, **{
  2268. "youtube": lambda d: 'https://www.youtube.com/watch?v=%s' % d['id'],
  2269. "soundcloud": lambda d: d['url'],
  2270. "bandcamp": lambda d: d['url']
  2271. })
  2272.  
  2273. exfunc = linegens[info['extractor'].split(':')[0]]
  2274.  
  2275. if not exfunc:
  2276. raise exceptions.CommandError("Could not extract info from input url, unsupported playlist type.", expire_in=25)
  2277.  
  2278. with BytesIO() as fcontent:
  2279. for item in info['entries']:
  2280. fcontent.write(exfunc(item).encode('utf8') + b'\n')
  2281.  
  2282. fcontent.seek(0)
  2283. await author.send("Here's the playlist dump for <%s>" % song_url, file=discord.File(fcontent, filename='playlist.txt'))
  2284.  
  2285. return Response("Sent a message with a playlist file.", delete_after=20)
  2286.  
  2287. async def cmd_listids(self, guild, author, leftover_args, cat='all'):
  2288. """
  2289. Usage:
  2290. {command_prefix}listids [categories]
  2291.  
  2292. Lists the ids for various things. Categories are:
  2293. all, users, roles, channels
  2294. """
  2295.  
  2296. cats = ['channels', 'roles', 'users']
  2297.  
  2298. if cat not in cats and cat != 'all':
  2299. return Response(
  2300. "Valid categories: " + ' '.join(['`%s`' % c for c in cats]),
  2301. reply=True,
  2302. delete_after=25
  2303. )
  2304.  
  2305. if cat == 'all':
  2306. requested_cats = cats
  2307. else:
  2308. requested_cats = [cat] + [c.strip(',') for c in leftover_args]
  2309.  
  2310. data = ['Your ID: %s' % author.id]
  2311.  
  2312. for cur_cat in requested_cats:
  2313. rawudata = None
  2314.  
  2315. if cur_cat == 'users':
  2316. data.append("\nUser IDs:")
  2317. rawudata = ['%s #%s: %s' % (m.name, m.discriminator, m.id) for m in guild.members]
  2318.  
  2319. elif cur_cat == 'roles':
  2320. data.append("\nRole IDs:")
  2321. rawudata = ['%s: %s' % (r.name, r.id) for r in guild.roles]
  2322.  
  2323. elif cur_cat == 'channels':
  2324. data.append("\nText Channel IDs:")
  2325. tchans = [c for c in guild.channels if isinstance(c, discord.TextChannel)]
  2326. rawudata = ['%s: %s' % (c.name, c.id) for c in tchans]
  2327.  
  2328. rawudata.append("\nVoice Channel IDs:")
  2329. vchans = [c for c in guild.channels if isinstance(c, discord.VoiceChannel)]
  2330. rawudata.extend('%s: %s' % (c.name, c.id) for c in vchans)
  2331.  
  2332. if rawudata:
  2333. data.extend(rawudata)
  2334.  
  2335. with BytesIO() as sdata:
  2336. sdata.writelines(d.encode('utf8') + b'\n' for d in data)
  2337. sdata.seek(0)
  2338.  
  2339. # TODO: Fix naming (Discord20API-ids.txt)
  2340. await author.send(file=discord.File(sdata, filename='%s-ids-%s.txt' % (guild.name.replace(' ', '_'), cat)))
  2341.  
  2342. return Response("Sent a message with a list of IDs.", delete_after=20)
  2343.  
  2344.  
  2345. async def cmd_perms(self, author, user_mentions, channel, guild, permissions):
  2346. """
  2347. Usage:
  2348. {command_prefix}perms [@user]
  2349.  
  2350. Sends the user a list of their permissions, or the permissions of the user specified.
  2351. """
  2352.  
  2353. lines = ['Command permissions in %s\n' % guild.name, '```', '```']
  2354.  
  2355. if user_mentions:
  2356. user = user_mentions[0]
  2357. permissions = self.permissions.for_user(user)
  2358.  
  2359. for perm in permissions.__dict__:
  2360. if perm in ['user_list'] or permissions.__dict__[perm] == set():
  2361. continue
  2362.  
  2363. lines.insert(len(lines) - 1, "%s: %s" % (perm, permissions.__dict__[perm]))
  2364.  
  2365. await self.safe_send_message(author, '\n'.join(lines))
  2366. return Response("\N{OPEN MAILBOX WITH RAISED FLAG}", delete_after=20)
  2367.  
  2368.  
  2369. @owner_only
  2370. async def cmd_setname(self, leftover_args, name):
  2371. """
  2372. Usage:
  2373. {command_prefix}setname name
  2374.  
  2375. Changes the bot's username.
  2376. Note: This operation is limited by discord to twice per hour.
  2377. """
  2378.  
  2379. name = ' '.join([name, *leftover_args])
  2380.  
  2381. try:
  2382. await self.user.edit(username=name)
  2383.  
  2384. except discord.HTTPException:
  2385. raise exceptions.CommandError(
  2386. "Failed to change name. Did you change names too many times? "
  2387. "Remember name changes are limited to twice per hour.")
  2388.  
  2389. except Exception as e:
  2390. raise exceptions.CommandError(e, expire_in=20)
  2391.  
  2392. return Response("Set the bot's username to **{0}**".format(name), delete_after=20)
  2393.  
  2394. async def cmd_setnick(self, guild, channel, leftover_args, nick):
  2395. """
  2396. Usage:
  2397. {command_prefix}setnick nick
  2398.  
  2399. Changes the bot's nickname.
  2400. """
  2401.  
  2402. if not channel.permissions_for(guild.me).change_nickname:
  2403. raise exceptions.CommandError("Unable to change nickname: no permission.")
  2404.  
  2405. nick = ' '.join([nick, *leftover_args])
  2406.  
  2407. try:
  2408. await guild.me.edit(nick=nick)
  2409. except Exception as e:
  2410. raise exceptions.CommandError(e, expire_in=20)
  2411.  
  2412. return Response("Set the bot's nickname to `{0}`".format(nick), delete_after=20)
  2413.  
  2414. @owner_only
  2415. async def cmd_setavatar(self, message, url=None):
  2416. """
  2417. Usage:
  2418. {command_prefix}setavatar [url]
  2419.  
  2420. Changes the bot's avatar.
  2421. Attaching a file and leaving the url parameter blank also works.
  2422. """
  2423.  
  2424. if message.attachments:
  2425. thing = message.attachments[0].url
  2426. elif url:
  2427. thing = url.strip('<>')
  2428. else:
  2429. raise exceptions.CommandError("You must provide a URL or attach a file.", expire_in=20)
  2430.  
  2431. try:
  2432. timeout = aiohttp.ClientTimeout(total=10)
  2433. async with self.aiosession.get(thing, timeout=timeout) as res:
  2434. await self.user.edit(avatar=await res.read())
  2435.  
  2436. except Exception as e:
  2437. raise exceptions.CommandError("Unable to change avatar: {}".format(e), expire_in=20)
  2438.  
  2439. return Response("Changed the bot's avatar.", delete_after=20)
  2440.  
  2441.  
  2442. async def cmd_disconnect(self, guild):
  2443. """
  2444. Usage:
  2445. {command_prefix}disconnect
  2446.  
  2447. Forces the bot leave the current voice channel.
  2448. """
  2449. await self.disconnect_voice_client(guild)
  2450. return Response("Disconnected from `{0.name}`".format(guild), delete_after=20)
  2451.  
  2452. async def cmd_restart(self, channel):
  2453. """
  2454. Usage:
  2455. {command_prefix}restart
  2456.  
  2457. Restarts the bot.
  2458. Will not properly load new dependencies or file updates unless fully shutdown
  2459. and restarted.
  2460. """
  2461. await self.safe_send_message(channel, "\N{WAVING HAND SIGN} Restarting. If you have updated your bot "
  2462. "or its dependencies, you need to restart the bot properly, rather than using this command.")
  2463.  
  2464. player = self.get_player_in(channel.guild)
  2465. if player and player.is_paused:
  2466. player.resume()
  2467.  
  2468. await self.disconnect_all_voice_clients()
  2469. raise exceptions.RestartSignal()
  2470.  
  2471. async def cmd_shutdown(self, channel):
  2472. """
  2473. Usage:
  2474. {command_prefix}shutdown
  2475.  
  2476. Disconnects from voice channels and closes the bot process.
  2477. """
  2478. await self.safe_send_message(channel, "\N{WAVING HAND SIGN}")
  2479.  
  2480. player = self.get_player_in(channel.guild)
  2481. if player and player.is_paused:
  2482. player.resume()
  2483.  
  2484. await self.disconnect_all_voice_clients()
  2485. raise exceptions.TerminateSignal()
  2486.  
  2487. async def cmd_leaveserver(self, val, leftover_args):
  2488. """
  2489. Usage:
  2490. {command_prefix}leaveserver <name/ID>
  2491.  
  2492. Forces the bot to leave a server.
  2493. When providing names, names are case-sensitive.
  2494. """
  2495. if leftover_args:
  2496. val = ' '.join([val, *leftover_args])
  2497.  
  2498. t = self.get_guild(val)
  2499. if t is None:
  2500. t = discord.utils.get(self.guilds, name=val)
  2501. if t is None:
  2502. raise exceptions.CommandError('No guild was found with the ID or name as `{0}`'.format(val))
  2503. await t.leave()
  2504. return Response('Left the guild: `{0.name}` (Owner: `{0.owner.name}`, ID: `{0.id}`)'.format(t))
  2505.  
  2506. @dev_only
  2507. async def cmd_breakpoint(self, message):
  2508. log.critical("Activating debug breakpoint")
  2509. return
  2510.  
  2511. @dev_only
  2512. async def cmd_objgraph(self, channel, func='most_common_types()'):
  2513. import objgraph
  2514.  
  2515. await self.send_typing(channel)
  2516.  
  2517. if func == 'growth':
  2518. f = StringIO()
  2519. objgraph.show_growth(limit=10, file=f)
  2520. f.seek(0)
  2521. data = f.read()
  2522. f.close()
  2523.  
  2524. elif func == 'leaks':
  2525. f = StringIO()
  2526. objgraph.show_most_common_types(objects=objgraph.get_leaking_objects(), file=f)
  2527. f.seek(0)
  2528. data = f.read()
  2529. f.close()
  2530.  
  2531. elif func == 'leakstats':
  2532. data = objgraph.typestats(objects=objgraph.get_leaking_objects())
  2533.  
  2534. else:
  2535. data = eval('objgraph.' + func)
  2536.  
  2537. return Response(data, codeblock='py')
  2538.  
  2539. @dev_only
  2540. async def cmd_debug(self, message, _player, *, data):
  2541. codeblock = "```py\n{}\n```"
  2542. result = None
  2543.  
  2544. if data.startswith('```') and data.endswith('```'):
  2545. data = '\n'.join(data.rstrip('`\n').split('\n')[1:])
  2546.  
  2547. code = data.strip('` \n')
  2548.  
  2549. try:
  2550. result = eval(code)
  2551. except:
  2552. try:
  2553. exec(code)
  2554. except Exception as e:
  2555. traceback.print_exc(chain=False)
  2556. return Response("{}: {}".format(type(e).__name__, e))
  2557.  
  2558. if asyncio.iscoroutine(result):
  2559. result = await result
  2560.  
  2561. return Response(codeblock.format(result))
  2562.  
  2563. async def on_message(self, message):
  2564. await self.wait_until_ready()
  2565.  
  2566. message_content = message.content.strip()
  2567. if not message_content.startswith(self.config.command_prefix):
  2568. return
  2569.  
  2570. if message.author == self.user:
  2571. log.warning("Ignoring command from myself ({})".format(message.content))
  2572. return
  2573.  
  2574. if self.config.bound_channels and message.channel.id not in self.config.bound_channels:
  2575. return # if I want to log this I just move it under the prefix check
  2576. if not isinstance(message.channel, discord.abc.GuildChannel):
  2577. return
  2578.  
  2579. command, *args = message_content.split(' ') # Uh, doesn't this break prefixes with spaces in them (it doesn't, config parser already breaks them)
  2580. command = command[len(self.config.command_prefix):].lower().strip()
  2581.  
  2582. args = ' '.join(args).lstrip(' ').split(' ')
  2583.  
  2584. handler = getattr(self, 'cmd_' + command, None)
  2585. if not handler:
  2586. return
  2587.  
  2588. if isinstance(message.channel, discord.abc.PrivateChannel):
  2589. if not (message.author.id == self.config.owner_id and command == 'joinserver'):
  2590. await self.send_message(message.channel, 'You cannot use this bot in private messages.')
  2591. return
  2592.  
  2593. if message.author.id in self.blacklist and message.author.id != self.config.owner_id:
  2594. log.warning("User blacklisted: {0.id}/{0!s} ({1})".format(message.author, command))
  2595. return
  2596.  
  2597. else:
  2598. log.info("{0.id}/{0!s}: {1}".format(message.author, message_content.replace('\n', '\n... ')))
  2599.  
  2600. user_permissions = self.permissions.for_user(message.author)
  2601.  
  2602. argspec = inspect.signature(handler)
  2603. params = argspec.parameters.copy()
  2604.  
  2605. sentmsg = response = None
  2606.  
  2607. # noinspection PyBroadException
  2608. try:
  2609. if user_permissions.ignore_non_voice and command in user_permissions.ignore_non_voice:
  2610. await self._check_ignore_non_voice(message)
  2611.  
  2612. handler_kwargs = {}
  2613. if params.pop('message', None):
  2614. handler_kwargs['message'] = message
  2615.  
  2616. if params.pop('channel', None):
  2617. handler_kwargs['channel'] = message.channel
  2618.  
  2619. if params.pop('author', None):
  2620. handler_kwargs['author'] = message.author
  2621.  
  2622. if params.pop('guild', None):
  2623. handler_kwargs['guild'] = message.guild
  2624.  
  2625. if params.pop('player', None):
  2626. handler_kwargs['player'] = await self.get_player(message.channel)
  2627.  
  2628. if params.pop('_player', None):
  2629. handler_kwargs['_player'] = self.get_player_in(message.guild)
  2630.  
  2631. if params.pop('permissions', None):
  2632. handler_kwargs['permissions'] = user_permissions
  2633.  
  2634. if params.pop('user_mentions', None):
  2635. handler_kwargs['user_mentions'] = list(map(message.guild.get_member, message.raw_mentions))
  2636.  
  2637. if params.pop('channel_mentions', None):
  2638. handler_kwargs['channel_mentions'] = list(map(message.guild.get_channel, message.raw_channel_mentions))
  2639.  
  2640. if params.pop('voice_channel', None):
  2641. handler_kwargs['voice_channel'] = message.guild.me.voice.channel if message.guild.me.voice else None
  2642.  
  2643. if params.pop('leftover_args', None):
  2644. handler_kwargs['leftover_args'] = args
  2645.  
  2646. args_expected = []
  2647. for key, param in list(params.items()):
  2648.  
  2649. # parse (*args) as a list of args
  2650. if param.kind == param.VAR_POSITIONAL:
  2651. handler_kwargs[key] = args
  2652. params.pop(key)
  2653. continue
  2654.  
  2655. # parse (*, args) as args rejoined as a string
  2656. # multiple of these arguments will have the same value
  2657. if param.kind == param.KEYWORD_ONLY and param.default == param.empty:
  2658. handler_kwargs[key] = ' '.join(args)
  2659. params.pop(key)
  2660. continue
  2661.  
  2662. doc_key = '[{}={}]'.format(key, param.default) if param.default is not param.empty else key
  2663. args_expected.append(doc_key)
  2664.  
  2665. # Ignore keyword args with default values when the command had no arguments
  2666. if not args and param.default is not param.empty:
  2667. params.pop(key)
  2668. continue
  2669.  
  2670. # Assign given values to positional arguments
  2671. if args:
  2672. arg_value = args.pop(0)
  2673. handler_kwargs[key] = arg_value
  2674. params.pop(key)
  2675.  
  2676. if message.author.id != self.config.owner_id:
  2677. if user_permissions.command_whitelist and command not in user_permissions.command_whitelist:
  2678. raise exceptions.PermissionsError(
  2679. "This command is not enabled for your group ({}).".format(user_permissions.name),
  2680. expire_in=20)
  2681.  
  2682. elif user_permissions.command_blacklist and command in user_permissions.command_blacklist:
  2683. raise exceptions.PermissionsError(
  2684. "This command is disabled for your group ({}).".format(user_permissions.name),
  2685. expire_in=20)
  2686.  
  2687. # Invalid usage, return docstring
  2688. if params:
  2689. docs = getattr(handler, '__doc__', None)
  2690. if not docs:
  2691. docs = 'Usage: {}{} {}'.format(
  2692. self.config.command_prefix,
  2693. command,
  2694. ' '.join(args_expected)
  2695. )
  2696.  
  2697. docs = dedent(docs)
  2698. await self.safe_send_message(
  2699. message.channel,
  2700. '```\n{}\n```'.format(docs.format(command_prefix=self.config.command_prefix)),
  2701. expire_in=60
  2702. )
  2703. return
  2704.  
  2705. response = await handler(**handler_kwargs)
  2706. if response and isinstance(response, Response):
  2707. if not isinstance(response.content, discord.Embed) and self.config.embeds:
  2708. content = self._gen_embed()
  2709. content.title = command
  2710. content.description = response.content
  2711. else:
  2712. content = response.content
  2713.  
  2714. if response.reply:
  2715. if isinstance(content, discord.Embed):
  2716. content.description = '{} {}'.format(message.author.mention, content.description if content.description is not discord.Embed.Empty else '')
  2717. else:
  2718. content = '{}: {}'.format(message.author.mention, content)
  2719.  
  2720. sentmsg = await self.safe_send_message(
  2721. message.channel, content,
  2722. expire_in=response.delete_after if self.config.delete_messages else 0,
  2723. also_delete=message if self.config.delete_invoking else None
  2724. )
  2725.  
  2726. except (exceptions.CommandError, exceptions.HelpfulError, exceptions.ExtractionError) as e:
  2727. log.error("Error in {0}: {1.__class__.__name__}: {1.message}".format(command, e), exc_info=True)
  2728.  
  2729. expirein = e.expire_in if self.config.delete_messages else None
  2730. alsodelete = message if self.config.delete_invoking else None
  2731.  
  2732. if self.config.embeds:
  2733. content = self._gen_embed()
  2734. content.add_field(name='Error', value=e.message, inline=False)
  2735. content.colour = 13369344
  2736. else:
  2737. content = '```\n{}\n```'.format(e.message)
  2738.  
  2739. await self.safe_send_message(
  2740. message.channel,
  2741. content,
  2742. expire_in=expirein,
  2743. also_delete=alsodelete
  2744. )
  2745.  
  2746. except exceptions.Signal:
  2747. raise
  2748.  
  2749. except Exception:
  2750. log.error("Exception in on_message", exc_info=True)
  2751. if self.config.debug_mode:
  2752. await self.safe_send_message(message.channel, '```\n{}\n```'.format(traceback.format_exc()))
  2753.  
  2754. finally:
  2755. if not sentmsg and not response and self.config.delete_invoking:
  2756. await asyncio.sleep(5)
  2757. await self.safe_delete_message(message, quiet=True)
  2758.  
  2759. async def gen_cmd_list(self, message, list_all_cmds=False):
  2760. for att in dir(self):
  2761. # This will always return at least cmd_help, since they needed perms to run this command
  2762. if att.startswith('cmd_') and not hasattr(getattr(self, att), 'dev_cmd'):
  2763. user_permissions = self.permissions.for_user(message.author)
  2764. command_name = att.replace('cmd_', '').lower()
  2765. whitelist = user_permissions.command_whitelist
  2766. blacklist = user_permissions.command_blacklist
  2767. if list_all_cmds:
  2768. self.commands.append('{}{}'.format(self.config.command_prefix, command_name))
  2769.  
  2770. elif blacklist and command_name in blacklist:
  2771. pass
  2772.  
  2773. elif whitelist and command_name not in whitelist:
  2774. pass
  2775.  
  2776. else:
  2777. self.commands.append("{}{}".format(self.config.command_prefix, command_name))
  2778.  
  2779. async def on_voice_state_update(self, member, before, after):
  2780. if not self.init_ok:
  2781. return # Ignore stuff before ready
  2782.  
  2783. if before.channel:
  2784. channel = before.channel
  2785. elif after.channel:
  2786. channel = after.channel
  2787. else:
  2788. return
  2789.  
  2790. if not self.config.auto_pause:
  2791. return
  2792.  
  2793. autopause_msg = "{state} in {channel.guild.name}/{channel.name} {reason}"
  2794.  
  2795. auto_paused = self.server_specific_data[channel.guild]['auto_paused']
  2796. player = await self.get_player(channel)
  2797.  
  2798. if not player:
  2799. return
  2800.  
  2801. if not member == self.user: # if the user is not the bot
  2802. if player.voice_client.channel != before.channel and player.voice_client.channel == after.channel: # if the person joined
  2803. if auto_paused and player.is_paused:
  2804. log.info(autopause_msg.format(
  2805. state = "Unpausing",
  2806. channel = player.voice_client.channel,
  2807. reason = ""
  2808. ).strip())
  2809.  
  2810. self.server_specific_data[player.voice_client.guild]['auto_paused'] = False
  2811. player.resume()
  2812. elif player.voice_client.channel == before.channel and player.voice_client.channel != after.channel:
  2813. if len(player.voice_client.channel.members) == 0:
  2814. if not auto_paused and player.is_playing:
  2815. log.info(autopause_msg.format(
  2816. state = "Pausing",
  2817. channel = player.voice_client.channel,
  2818. reason = "(empty channel)"
  2819. ).strip())
  2820.  
  2821. self.server_specific_data[player.voice_client.guild]['auto_paused'] = True
  2822. player.pause()
  2823. else:
  2824. if len(player.voice_client.channel.members) > 0: # channel is not empty
  2825. if auto_paused and player.is_paused:
  2826. log.info(autopause_msg.format(
  2827. state = "Unpausing",
  2828. channel = player.voice_client.channel,
  2829. reason = ""
  2830. ).strip())
  2831.  
  2832. self.server_specific_data[player.voice_client.guild]['auto_paused'] = False
  2833. player.resume()
  2834.  
  2835. async def on_guild_update(self, before:discord.Guild, after:discord.Guild):
  2836. if before.region != after.region:
  2837. log.warning("Guild \"%s\" changed regions: %s -> %s" % (after.name, before.region, after.region))
  2838.  
  2839. async def on_guild_join(self, guild:discord.Guild):
  2840. log.info("Bot has been joined guild: {}".format(guild.name))
  2841.  
  2842. log.debug("Creating data folder for guild %s", guild.id)
  2843. pathlib.Path('data/%s/' % guild.id).mkdir(exist_ok=True)
  2844.  
  2845. async def on_guild_remove(self, guild:discord.Guild):
  2846. log.info("Bot has been removed from guild: {}".format(guild.name))
  2847. log.debug('Updated guild list:')
  2848. [log.debug(' - ' + s.name) for s in self.guilds]
  2849.  
  2850. if guild.id in self.players:
  2851. self.players.pop(guild.id).kill()
  2852.  
  2853.  
  2854. async def on_guild_available(self, guild:discord.Guild):
  2855. if not self.init_ok:
  2856. return # Ignore pre-ready events
  2857.  
  2858. log.debug("Guild \"{}\" has become available.".format(guild.name))
  2859.  
  2860. player = self.get_player_in(guild)
  2861.  
  2862. if player and player.is_paused:
  2863. av_paused = self.server_specific_data[guild]['availability_paused']
  2864.  
  2865. if av_paused:
  2866. log.debug("Resuming player in \"{}\" due to availability.".format(guild.name))
  2867. self.server_specific_data[guild]['availability_paused'] = False
  2868. player.resume()
  2869.  
  2870.  
  2871. async def on_server_unavailable(self, guild:discord.Guild):
  2872. log.debug("Guild \"{}\" has become unavailable.".format(guild.name))
  2873.  
  2874. player = self.get_player_in(guild)
  2875.  
  2876. if player and player.is_playing:
  2877. log.debug("Pausing player in \"{}\" due to unavailability.".format(guild.name))
  2878. self.server_specific_data[guild]['availability_paused'] = True
  2879. player.pause()
  2880.  
  2881. def voice_client_in(self, guild):
  2882. for vc in self.voice_clients:
  2883. if vc.guild == guild:
  2884. return vc
  2885. return None
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement