Browse Source

Typo corrections, upgrades from Py2 to Py3-compatible formatting, patch yaml loader deprecation warning, Discord media proxy links (allows caching of possibly deleted images, relies less on the CDN)

pull/129/head
“elderlabs” 7 years ago
parent
commit
5695eff84f
  1. 12
      disco/api/http.py
  2. 2
      disco/api/ratelimit.py
  3. 12
      disco/bot/bot.py
  4. 4
      disco/bot/plugin.py
  5. 2
      disco/cli.py
  6. 20
      disco/gateway/client.py
  7. 2
      disco/state.py
  8. 2
      disco/types/base.py
  9. 6
      disco/types/channel.py
  10. 8
      disco/types/guild.py
  11. 6
      disco/types/user.py
  12. 5
      disco/util/serializer.py
  13. 40
      disco/voice/client.py
  14. 2
      disco/voice/playable.py
  15. 20
      disco/voice/udp.py
  16. 12
      docs/bot_tutorial/building_block_listeners.md
  17. 8
      setup.py

12
disco/api/http.py

@ -52,8 +52,7 @@ class Routes(object):
CHANNELS_MESSAGES_REACTIONS_GET = (HTTPMethod.GET, CHANNELS + '/messages/{message}/reactions/{emoji}') CHANNELS_MESSAGES_REACTIONS_GET = (HTTPMethod.GET, CHANNELS + '/messages/{message}/reactions/{emoji}')
CHANNELS_MESSAGES_REACTIONS_CREATE = (HTTPMethod.PUT, CHANNELS + '/messages/{message}/reactions/{emoji}/@me') CHANNELS_MESSAGES_REACTIONS_CREATE = (HTTPMethod.PUT, CHANNELS + '/messages/{message}/reactions/{emoji}/@me')
CHANNELS_MESSAGES_REACTIONS_DELETE_ME = (HTTPMethod.DELETE, CHANNELS + '/messages/{message}/reactions/{emoji}/@me') CHANNELS_MESSAGES_REACTIONS_DELETE_ME = (HTTPMethod.DELETE, CHANNELS + '/messages/{message}/reactions/{emoji}/@me')
CHANNELS_MESSAGES_REACTIONS_DELETE_USER = (HTTPMethod.DELETE, CHANNELS_MESSAGES_REACTIONS_DELETE_USER = (HTTPMethod.DELETE, CHANNELS + '/messages/{message}/reactions/{emoji}/{user}')
CHANNELS + '/messages/{message}/reactions/{emoji}/{user}')
CHANNELS_PERMISSIONS_MODIFY = (HTTPMethod.PUT, CHANNELS + '/permissions/{permission}') CHANNELS_PERMISSIONS_MODIFY = (HTTPMethod.PUT, CHANNELS + '/permissions/{permission}')
CHANNELS_PERMISSIONS_DELETE = (HTTPMethod.DELETE, CHANNELS + '/permissions/{permission}') CHANNELS_PERMISSIONS_DELETE = (HTTPMethod.DELETE, CHANNELS + '/permissions/{permission}')
CHANNELS_INVITES_LIST = (HTTPMethod.GET, CHANNELS + '/invites') CHANNELS_INVITES_LIST = (HTTPMethod.GET, CHANNELS + '/invites')
@ -264,11 +263,11 @@ class HTTPClient(LoggingClass):
# Possibly wait if we're rate limited # Possibly wait if we're rate limited
response.rate_limited_duration = self.limiter.check(bucket) response.rate_limited_duration = self.limiter.check(bucket)
self.log.debug('KW: %s', kwargs) self.log.debug('KW: {}'.format(kwargs))
# Make the actual request # Make the actual request
url = self.BASE_URL + route[1].format(**args) url = self.BASE_URL + route[1].format(**args)
self.log.info('%s %s (%s)', route[0].value, url, kwargs.get('params')) self.log.info('{} {} ({})'.format(route[0].value, url, kwargs.get('params')))
r = self.session.request(route[0].value, url, **kwargs) r = self.session.request(route[0].value, url, **kwargs)
if self.after_request: if self.after_request:
@ -282,13 +281,12 @@ class HTTPClient(LoggingClass):
if r.status_code < 400: if r.status_code < 400:
return r return r
elif r.status_code != 429 and 400 <= r.status_code < 500: elif r.status_code != 429 and 400 <= r.status_code < 500:
self.log.warning('Request failed with code %s: %s', r.status_code, r.content) self.log.warning('Request failed with code {}: {}'.format(r.status_code, r.content))
response.exception = APIException(r) response.exception = APIException(r)
raise response.exception raise response.exception
else: else:
if r.status_code == 429: if r.status_code == 429:
self.log.warning( self.log.warning('Request responded w/ 429, retrying (but this should not happen, check your clock sync')
'Request responded w/ 429, retrying (but this should not happen, check your clock sync')
# If we hit the max retries, throw an error # If we hit the max retries, throw an error
retry += 1 retry += 1

2
disco/api/ratelimit.py

@ -99,7 +99,7 @@ class RouteState(LoggingClass):
self.event = gevent.event.Event() self.event = gevent.event.Event()
delay = (self.reset_time - time.time()) + .5 delay = (self.reset_time - time.time()) + .5
self.log.debug('Cooling down bucket %s for %s seconds', self, delay) self.log.debug('Cooling down bucket {} for {} seconds'.format(self, delay))
gevent.sleep(delay) gevent.sleep(delay)
self.event.set() self.event.set()
self.event = None self.event = None

12
disco/bot/bot.py

@ -154,7 +154,7 @@ class Bot(LoggingClass):
except ImportError: except ImportError:
self.log.warning('Failed to enable HTTP server, Flask is not installed') self.log.warning('Failed to enable HTTP server, Flask is not installed')
else: else:
self.log.info('Starting HTTP server bound to %s:%s', self.config.http_host, self.config.http_port) self.log.info('Starting HTTP server bound to {}:{}'.format(self.config.http_host, self.config.http_port))
self.http = Flask('disco') self.http = Flask('disco')
self.http_server = WSGIServer((self.config.http_host, self.config.http_port), self.http) self.http_server = WSGIServer((self.config.http_host, self.config.http_port), self.http)
self.http_server_greenlet = gevent.spawn(self.http_server.serve_forever) self.http_server_greenlet = gevent.spawn(self.http_server.serve_forever)
@ -234,7 +234,7 @@ class Bot(LoggingClass):
Computes all possible abbreviations for a command grouping. Computes all possible abbreviations for a command grouping.
""" """
# For the first pass, we just want to compute each groups possible # For the first pass, we just want to compute each groups possible
# abbreviations that don't conflict with eachother. # abbreviations that don't conflict with each other.
possible = {} possible = {}
for group in groups: for group in groups:
for index in range(1, len(group)): for index in range(1, len(group)):
@ -244,7 +244,7 @@ class Bot(LoggingClass):
else: else:
possible[current] = group possible[current] = group
# Now, we want to compute the actual shortest abbreivation out of the # Now, we want to compute the actual shortest abbreviation out of the
# possible ones # possible ones
result = {} result = {}
for abbrev, group in six.iteritems(possible): for abbrev, group in six.iteritems(possible):
@ -449,7 +449,7 @@ class Bot(LoggingClass):
inst = inst(self, config) inst = inst(self, config)
if inst.__class__.__name__ in self.plugins: if inst.__class__.__name__ in self.plugins:
self.log.warning('Attempted to add already added plugin %s', inst.__class__.__name__) self.log.warning('Attempted to add already added plugin {}'.format(inst.__class__.__name__))
raise Exception('Cannot add already added plugin: {}'.format(inst.__class__.__name__)) raise Exception('Cannot add already added plugin: {}'.format(inst.__class__.__name__))
self.ctx['plugin'] = self.plugins[inst.__class__.__name__] = inst self.ctx['plugin'] = self.plugins[inst.__class__.__name__] = inst
@ -467,7 +467,7 @@ class Bot(LoggingClass):
Plugin class to unload and remove. Plugin class to unload and remove.
""" """
if cls.__name__ not in self.plugins: if cls.__name__ not in self.plugins:
raise Exception('Cannot remove non-existant plugin: {}'.format(cls.__name__)) raise Exception('Cannot remove non-existent plugin: {}'.format(cls.__name__))
ctx = {} ctx = {}
self.plugins[cls.__name__].unload(ctx) self.plugins[cls.__name__].unload(ctx)
@ -495,7 +495,7 @@ class Bot(LoggingClass):
""" """
Adds and loads a plugin, based on its module path. Adds and loads a plugin, based on its module path.
""" """
self.log.info('Adding plugin module at path "%s"', path) self.log.info('Adding plugin module at path "{}"'.format(path))
mod = importlib.import_module(path) mod = importlib.import_module(path)
loaded = False loaded = False

4
disco/bot/plugin.py

@ -66,7 +66,7 @@ def find_loadable_plugins(mod):
class BasePluginDeco(object): class BasePluginDeco(object):
Prio = Priority Prio = Priority
# TODO: dont smash class methods # TODO: don't smash class methods
@classmethod @classmethod
def add_meta_deco(cls, meta): def add_meta_deco(cls, meta):
def deco(f): def deco(f):
@ -234,7 +234,7 @@ class Plugin(LoggingClass, PluginDeco):
self.storage = bot.storage self.storage = bot.storage
self.config = config self.config = config
# General declartions # General declarations
self.listeners = [] self.listeners = []
self.commands = [] self.commands = []
self.schedules = {} self.schedules = {}

2
disco/cli.py

@ -103,7 +103,7 @@ def disco_main(run=False):
if run: if run:
(bot or client).run_forever() (bot or client).run_forever()
return (bot or client) return bot or client
if __name__ == '__main__': if __name__ == '__main__':

20
disco/gateway/client.py

@ -75,7 +75,7 @@ class GatewayClient(LoggingClass):
return self._send(op, data) return self._send(op, data)
def _send(self, op, data): def _send(self, op, data):
self.log.debug('GatewayClient.send %s', op) self.log.debug('GatewayClient.send {}'.format(op))
self.packets.emit((SEND, op), data) self.packets.emit((SEND, op), data)
self.ws.send(self.encoder.encode({ self.ws.send(self.encoder.encode({
'op': op.value, 'op': op.value,
@ -96,7 +96,7 @@ class GatewayClient(LoggingClass):
def handle_dispatch(self, packet): def handle_dispatch(self, packet):
obj = GatewayEvent.from_dispatch(self.client, packet) obj = GatewayEvent.from_dispatch(self.client, packet)
self.log.debug('GatewayClient.handle_dispatch %s', obj.__class__.__name__) self.log.debug('GatewayClient.handle_dispatch {}'.format(obj.__class__.__name__))
self.client.events.emit(obj.__class__.__name__, obj) self.client.events.emit(obj.__class__.__name__, obj)
if self.replaying: if self.replaying:
self.replayed_events += 1 self.replayed_events += 1
@ -119,7 +119,7 @@ class GatewayClient(LoggingClass):
self.ws.close() self.ws.close()
def handle_hello(self, packet): def handle_hello(self, packet):
self.log.info('Received HELLO, starting heartbeater...') self.log.info('Received HELLO, starting heartbeat...')
self._heartbeat_task = gevent.spawn(self.heartbeat_task, packet['d']['heartbeat_interval']) self._heartbeat_task = gevent.spawn(self.heartbeat_task, packet['d']['heartbeat_interval'])
def on_ready(self, ready): def on_ready(self, ready):
@ -128,7 +128,7 @@ class GatewayClient(LoggingClass):
self.reconnects = 0 self.reconnects = 0
def on_resumed(self, _): def on_resumed(self, _):
self.log.info('RESUME completed, replayed %s events', self.replayed_events) self.log.info('RESUME completed, replayed {} events'.format(self.replayed_events))
self.reconnects = 0 self.reconnects = 0
self.replaying = False self.replaying = False
@ -144,7 +144,7 @@ class GatewayClient(LoggingClass):
if self.zlib_stream_enabled: if self.zlib_stream_enabled:
gateway_url += '&compress=zlib-stream' gateway_url += '&compress=zlib-stream'
self.log.info('Opening websocket connection to URL `%s`', gateway_url) self.log.info('Opening websocket connection to URL `{}`'.format(gateway_url))
self.ws = Websocket(gateway_url) self.ws = Websocket(gateway_url)
self.ws.emitter.on('on_open', self.on_open) self.ws.emitter.on('on_open', self.on_open)
self.ws.emitter.on('on_error', self.on_error) self.ws.emitter.on('on_error', self.on_error)
@ -194,14 +194,14 @@ class GatewayClient(LoggingClass):
if isinstance(error, KeyboardInterrupt): if isinstance(error, KeyboardInterrupt):
self.shutting_down = True self.shutting_down = True
self.ws_event.set() self.ws_event.set()
raise Exception('WS recieved error: %s', error) raise Exception('WS received error: {}'.format(error))
def on_open(self): def on_open(self):
if self.zlib_stream_enabled: if self.zlib_stream_enabled:
self._zlib = zlib.decompressobj() self._zlib = zlib.decompressobj()
if self.seq and self.session_id: if self.seq and self.session_id:
self.log.info('WS Opened: attempting resume w/ SID: %s SEQ: %s', self.session_id, self.seq) self.log.info('WS Opened: attempting resume w/ SID: {} SEQ: {}'.format(self.session_id, self.seq))
self.replaying = True self.replaying = True
self.send(OPCode.RESUME, { self.send(OPCode.RESUME, {
'token': self.client.config.token, 'token': self.client.config.token,
@ -230,7 +230,7 @@ class GatewayClient(LoggingClass):
# Make sure we cleanup any old data # Make sure we cleanup any old data
self._buffer = None self._buffer = None
# Kill heartbeater, a reconnect/resume will trigger a HELLO which will # Kill heartbeat, a reconnect/resume will trigger a HELLO which will
# respawn it # respawn it
if self._heartbeat_task: if self._heartbeat_task:
self._heartbeat_task.kill() self._heartbeat_task.kill()
@ -244,7 +244,7 @@ class GatewayClient(LoggingClass):
# Track reconnect attempts # Track reconnect attempts
self.reconnects += 1 self.reconnects += 1
self.log.info('WS Closed: [%s] %s (%s)', code, reason, self.reconnects) self.log.info('WS Closed: [{}] {} ({})'.format(code, reason, self.reconnects))
if self.max_reconnects and self.reconnects > self.max_reconnects: if self.max_reconnects and self.reconnects > self.max_reconnects:
raise Exception('Failed to reconnect after {} attempts, giving up'.format(self.max_reconnects)) raise Exception('Failed to reconnect after {} attempts, giving up'.format(self.max_reconnects))
@ -254,7 +254,7 @@ class GatewayClient(LoggingClass):
self.session_id = None self.session_id = None
wait_time = self.reconnects * 5 wait_time = self.reconnects * 5
self.log.info('Will attempt to %s after %s seconds', 'resume' if self.session_id else 'reconnect', wait_time) self.log.info('Will attempt to {} after {} seconds'.format('resume' if self.session_id else 'reconnect', wait_time))
gevent.sleep(wait_time) gevent.sleep(wait_time)
# Reconnect # Reconnect

2
disco/state.py

@ -214,7 +214,7 @@ class State(object):
def on_guild_delete(self, event): def on_guild_delete(self, event):
if event.id in self.guilds: if event.id in self.guilds:
# Just delete the guild, channel references will fall # Just delete the guild, channel references will fail
del self.guilds[event.id] del self.guilds[event.id]
if event.id in self.voice_clients: if event.id in self.voice_clients:

2
disco/types/base.py

@ -215,7 +215,7 @@ def datetime(data):
except (ValueError, TypeError): except (ValueError, TypeError):
continue continue
raise ValueError('Failed to conver `{}` to datetime'.format(data)) raise ValueError('Failed to convert `{}` to datetime'.format(data))
def text(obj): def text(obj):

6
disco/types/channel.py

@ -457,21 +457,21 @@ class Channel(SlottedModel, Permissible):
""" """
Sets the channels bitrate. Sets the channels bitrate.
""" """
assert (self.is_voice) assert self.is_voice
return self.client.api.channels_modify(self.id, bitrate=bitrate, reason=reason) return self.client.api.channels_modify(self.id, bitrate=bitrate, reason=reason)
def set_user_limit(self, user_limit, reason=None): def set_user_limit(self, user_limit, reason=None):
""" """
Sets the channels user limit. Sets the channels user limit.
""" """
assert (self.is_voice) assert self.is_voice
return self.client.api.channels_modify(self.id, user_limit=user_limit, reason=reason) return self.client.api.channels_modify(self.id, user_limit=user_limit, reason=reason)
def set_parent(self, parent, reason=None): def set_parent(self, parent, reason=None):
""" """
Sets the channels parent. Sets the channels parent.
""" """
assert (self.is_guild) assert self.is_guild
return self.client.api.channels_modify( return self.client.api.channels_modify(
self.id, self.id,
parent_id=to_snowflake(parent) if parent else parent, parent_id=to_snowflake(parent) if parent else parent,

8
disco/types/guild.py

@ -75,7 +75,7 @@ class GuildEmoji(Emoji):
@property @property
def url(self): def url(self):
return 'https://discordapp.com/api/emojis/{}.{}'.format(self.id, 'gif' if self.animated else 'png') return 'https://media.discordapp.net/emojis/{}.{}'.format(self.id, 'gif' if self.animated else 'png')
@cached_property @cached_property
def guild(self): def guild(self):
@ -505,19 +505,19 @@ class Guild(SlottedModel, Permissible):
if not self.icon: if not self.icon:
return '' return ''
return 'https://cdn.discordapp.com/icons/{}/{}.{}?size={}'.format(self.id, self.icon, fmt, size) return 'https://media.discordapp.net/icons/{}/{}.{}?size={}'.format(self.id, self.icon, fmt, size)
def get_splash_url(self, fmt='webp', size=1024): def get_splash_url(self, fmt='webp', size=1024):
if not self.splash: if not self.splash:
return '' return ''
return 'https://cdn.discordapp.com/splashes/{}/{}.{}?size={}'.format(self.id, self.splash, fmt, size) return 'https://media.discordapp.net/splashes/{}/{}.{}?size={}'.format(self.id, self.splash, fmt, size)
def get_banner_url(self, fmt='webp', size=1024): def get_banner_url(self, fmt='webp', size=1024):
if not self.banner: if not self.banner:
return '' return ''
return 'https://cdn.discordapp.com/banners/{}/{}.{}?size={}'.format(self.id, self.banner, fmt, size) return 'https://media.discordapp.net/banners/{}/{}.{}?size={}'.format(self.id, self.banner, fmt, size)
@property @property
def icon_url(self): def icon_url(self):

6
disco/types/user.py

@ -26,11 +26,11 @@ class User(SlottedModel, with_equality('id'), with_hash('id')):
if not self.avatar: if not self.avatar:
return 'https://cdn.discordapp.com/embed/avatars/{}.png'.format(self.default_avatar.value) return 'https://cdn.discordapp.com/embed/avatars/{}.png'.format(self.default_avatar.value)
if fmt is not None: if fmt is not None:
return 'https://cdn.discordapp.com/avatars/{}/{}.{}?size={}'.format(self.id, self.avatar, fmt, size) return 'https://media.discordapp.net/avatars/{}/{}.{}?size={}'.format(self.id, self.avatar, fmt, size)
if self.avatar.startswith('a_'): if self.avatar.startswith('a_'):
return 'https://cdn.discordapp.com/avatars/{}/{}.gif?size={}'.format(self.id, self.avatar, size) return 'https://media.discordapp.net/avatars/{}/{}.gif?size={}'.format(self.id, self.avatar, size)
else: else:
return 'https://cdn.discordapp.com/avatars/{}/{}.webp?size={}'.format(self.id, self.avatar, size) return 'https://media.discordapp.net/avatars/{}/{}.webp?size={}'.format(self.id, self.avatar, size)
@property @property
def default_avatar(self): def default_avatar(self):

5
disco/util/serializer.py

@ -12,7 +12,7 @@ class Serializer(object):
@classmethod @classmethod
def check_format(cls, fmt): def check_format(cls, fmt):
if fmt not in cls.FORMATS: if fmt not in cls.FORMATS:
raise Exception('Unsupported serilization format: {}'.format(fmt)) raise Exception('Unsupported serialization format: {}'.format(fmt))
@staticmethod @staticmethod
def json(): def json():
@ -31,8 +31,9 @@ class Serializer(object):
@classmethod @classmethod
def loads(cls, fmt, raw): def loads(cls, fmt, raw):
import yaml
loads, _ = getattr(cls, fmt)() loads, _ = getattr(cls, fmt)()
return loads(raw) return loads(raw, Loader=yaml.FullLoader)
@classmethod @classmethod
def dumps(cls, fmt, raw): def dumps(cls, fmt, raw):

40
disco/voice/client.py

@ -143,7 +143,7 @@ class VoiceClient(LoggingClass):
return self.ssrc + 3 return self.ssrc + 3
def set_state(self, state): def set_state(self, state):
self.log.debug('[%s] state %s -> %s', self, self.state, state) self.log.debug('[{}] state {} -> {}'.format(self, self.state, state))
prev_state = self.state prev_state = self.state
self.state = state self.state = state
self.state_emitter.emit(state, prev_state) self.state_emitter.emit(state, prev_state)
@ -154,7 +154,7 @@ class VoiceClient(LoggingClass):
return return
self.log.info( self.log.info(
'[%s] Set endpoint from VOICE_SERVER_UPDATE (state = %s / endpoint = %s)', self, self.state, endpoint) '[{}] Set endpoint from VOICE_SERVER_UPDATE (state = {} / endpoint = {})'.format(self, self.state, endpoint))
self.endpoint = endpoint self.endpoint = endpoint
@ -210,13 +210,13 @@ class VoiceClient(LoggingClass):
def send(self, op, data): def send(self, op, data):
if self.ws and self.ws.sock and self.ws.sock.connected: if self.ws and self.ws.sock and self.ws.sock.connected:
self.log.debug('[%s] sending OP %s (data = %s)', self, op, data) self.log.debug('[{}] sending OP {} (data = {})'.format(self, op, data))
self.ws.send(self.encoder.encode({ self.ws.send(self.encoder.encode({
'op': op.value, 'op': op.value,
'd': data, 'd': data,
}), self.encoder.OPCODE) }), self.encoder.OPCODE)
else: else:
self.log.debug('[%s] dropping because ws is closed OP %s (data = %s)', self, op, data) self.log.debug('[{}] dropping because ws is closed OP {} (data = {})'.format(self, op, data))
def on_voice_client_connect(self, data): def on_voice_client_connect(self, data):
user_id = int(data['user_id']) user_id = int(data['user_id'])
@ -241,12 +241,12 @@ class VoiceClient(LoggingClass):
self.udp.set_audio_codec(data['audio_codec']) self.udp.set_audio_codec(data['audio_codec'])
def on_voice_hello(self, data): def on_voice_hello(self, data):
self.log.info('[%s] Received Voice HELLO payload, starting heartbeater', self) self.log.info('[{}] Received Voice HELLO payload, starting heartbeat'.format(self))
self._heartbeat_task = gevent.spawn(self._heartbeat, data['heartbeat_interval']) self._heartbeat_task = gevent.spawn(self._heartbeat, data['heartbeat_interval'])
self.set_state(VoiceState.AUTHENTICATED) self.set_state(VoiceState.AUTHENTICATED)
def on_voice_ready(self, data): def on_voice_ready(self, data):
self.log.info('[%s] Received Voice READY payload, attempting to negotiate voice connection w/ remote', self) self.log.info('[{}] Received Voice READY payload, attempting to negotiate voice connection w/ remote'.format(self))
self.set_state(VoiceState.CONNECTING) self.set_state(VoiceState.CONNECTING)
self.ssrc = data['ssrc'] self.ssrc = data['ssrc']
self.ip = data['ip'] self.ip = data['ip']
@ -256,12 +256,12 @@ class VoiceClient(LoggingClass):
for mode in self.SUPPORTED_MODES: for mode in self.SUPPORTED_MODES:
if mode in data['modes']: if mode in data['modes']:
self.mode = mode self.mode = mode
self.log.debug('[%s] Selected mode %s', self, mode) self.log.debug('[{}] Selected mode {}'.format(self, mode))
break break
else: else:
raise Exception('Failed to find a supported voice mode') raise Exception('Failed to find a supported voice mode')
self.log.debug('[%s] Attempting IP discovery over UDP to %s:%s', self, self.ip, self.port) self.log.debug('[{}] Attempting IP discovery over UDP to {}:{}'.format(self, self.ip, self.port))
self.udp = UDPVoiceClient(self) self.udp = UDPVoiceClient(self)
ip, port = self.udp.connect(self.ip, self.port) ip, port = self.udp.connect(self.ip, self.port)
@ -281,7 +281,7 @@ class VoiceClient(LoggingClass):
'payload_type': RTPPayloadTypes.get(codec).value, 'payload_type': RTPPayloadTypes.get(codec).value,
}) })
self.log.debug('[%s] IP discovery completed (ip = %s, port = %s), sending SELECT_PROTOCOL', self, ip, port) self.log.debug('[{}] IP discovery completed (ip = {}, port = {}), sending SELECT_PROTOCOL'.format(self, ip, port))
self.send(VoiceOPCode.SELECT_PROTOCOL, { self.send(VoiceOPCode.SELECT_PROTOCOL, {
'protocol': 'udp', 'protocol': 'udp',
'data': { 'data': {
@ -298,11 +298,11 @@ class VoiceClient(LoggingClass):
}) })
def on_voice_resumed(self, data): def on_voice_resumed(self, data):
self.log.info('[%s] Received resumed', self) self.log.info('[{}] Received resumed'.format(self))
self.set_state(VoiceState.CONNECTED) self.set_state(VoiceState.CONNECTED)
def on_voice_sdp(self, sdp): def on_voice_sdp(self, sdp):
self.log.info('[%s] Received session description, connection completed', self) self.log.info('[{}] Received session description, connection completed'.format(self))
self.mode = sdp['mode'] self.mode = sdp['mode']
self.audio_codec = sdp['audio_codec'] self.audio_codec = sdp['audio_codec']
@ -341,7 +341,7 @@ class VoiceClient(LoggingClass):
self.log.exception('Failed to parse voice gateway message: ') self.log.exception('Failed to parse voice gateway message: ')
def on_error(self, err): def on_error(self, err):
self.log.error('[%s] Voice websocket error: %s', self, err) self.log.error('[{}] Voice websocket error: {}'.format(self, err))
def on_open(self): def on_open(self):
if self._identified: if self._identified:
@ -360,7 +360,7 @@ class VoiceClient(LoggingClass):
}) })
def on_close(self, code, reason): def on_close(self, code, reason):
self.log.warning('[%s] Voice websocket closed: [%s] %s (%s)', self, code, reason, self._reconnects) self.log.warning('[{}] Voice websocket closed: [{}] {} ({})'.format(self, code, reason, self._reconnects))
if self._heartbeat_task: if self._heartbeat_task:
self._heartbeat_task.kill() self._heartbeat_task.kill()
@ -372,7 +372,7 @@ class VoiceClient(LoggingClass):
if self.state == VoiceState.DISCONNECTED: if self.state == VoiceState.DISCONNECTED:
return return
self.log.info('[%s] Attempting Websocket Resumption', self) self.log.info('[{}] Attempting Websocket Resumption'.format(self))
self.set_state(VoiceState.RECONNECTING) self.set_state(VoiceState.RECONNECTING)
@ -396,7 +396,7 @@ class VoiceClient(LoggingClass):
wait_time = 1 wait_time = 1
self.log.info( self.log.info(
'[%s] Will attempt to %s after %s seconds', self, 'resume' if self._identified else 'reconnect', wait_time) '[{}] Will attempt to {} after {} seconds'.format(self, 'resume' if self._identified else 'reconnect', wait_time))
gevent.sleep(wait_time) gevent.sleep(wait_time)
self._connect_and_run() self._connect_and_run()
@ -406,17 +406,17 @@ class VoiceClient(LoggingClass):
channel_id = self.server_id channel_id = self.server_id
if not channel_id: if not channel_id:
raise VoiceException('[%s] cannot connect to an empty channel id', self) raise VoiceException('[{}] cannot connect to an empty channel id'.format(self))
if self.channel_id == channel_id: if self.channel_id == channel_id:
if self.state == VoiceState.CONNECTED: if self.state == VoiceState.CONNECTED:
self.log.debug('[%s] Already connected to %s, returning', self, self.channel) self.log.debug('[{}] Already connected to {}, returning'.format(self, self.channel))
return self return self
else: else:
if self.state == VoiceState.CONNECTED: if self.state == VoiceState.CONNECTED:
self.log.debug('[%s] Moving to channel %s', self, channel_id) self.log.debug('[{}] Moving to channel {}'.format(self, channel_id))
else: else:
self.log.debug('[%s] Attempting connection to channel id %s', self, channel_id) self.log.debug('[{}] Attempting connection to channel id {}'.format(self, channel_id))
self.set_state(VoiceState.AWAITING_ENDPOINT) self.set_state(VoiceState.AWAITING_ENDPOINT)
self.set_voice_state(channel_id, **kwargs) self.set_voice_state(channel_id, **kwargs)
@ -431,7 +431,7 @@ class VoiceClient(LoggingClass):
if self.state == VoiceState.DISCONNECTED: if self.state == VoiceState.DISCONNECTED:
return return
self.log.debug('[%s] disconnect called', self) self.log.debug('[{}] disconnect called'.format(self))
self.set_state(VoiceState.DISCONNECTED) self.set_state(VoiceState.DISCONNECTED)
del self.client.state.voice_clients[self.server_id] del self.client.state.voice_clients[self.server_id]

2
disco/voice/playable.py

@ -12,7 +12,7 @@ from disco.voice.opus import OpusEncoder
try: try:
from cStringIO import cStringIO as BufferedIO from io import StringIO as BufferedIO
except ImportError: except ImportError:
if six.PY2: if six.PY2:
from StringIO import StringIO as BufferedIO from StringIO import StringIO as BufferedIO

20
disco/voice/udp.py

@ -7,7 +7,7 @@ from collections import namedtuple
try: try:
import nacl.secret import nacl.secret
except ImportError: except ImportError:
print('WARNING: nacl is not installed, voice support is disabled') print('WARNING: PyNaCl is not installed, voice support is disabled')
from holster.enum import Enum from holster.enum import Enum
@ -103,7 +103,7 @@ class UDPVoiceClient(LoggingClass):
ptype = RTPPayloadTypes.get(codec) ptype = RTPPayloadTypes.get(codec)
self._rtp_audio_header[1] = ptype.value self._rtp_audio_header[1] = ptype.value
self.log.debug('[%s] Set UDP\'s Audio Codec to %s, RTP payload type %s', self.vc, ptype.name, ptype.value) self.log.debug('[{}] Set UDP\'s Audio Codec to {}, RTP payload type {}'.format(self.vc, ptype.name, ptype.value))
def increment_timestamp(self, by): def increment_timestamp(self, by):
self.timestamp += by self.timestamp += by
@ -168,7 +168,7 @@ class UDPVoiceClient(LoggingClass):
# Data cannot be less than the bare minimum, just ignore # Data cannot be less than the bare minimum, just ignore
if len(data) <= 12: if len(data) <= 12:
self.log.debug('[%s] [VoiceData] Received voice data under 13 bytes', self.vc) self.log.debug('[{}] [VoiceData] Received voice data under 13 bytes'.format(self.vc))
continue continue
first, second = struct.unpack_from('>BB', data) first, second = struct.unpack_from('>BB', data)
@ -220,14 +220,14 @@ class UDPVoiceClient(LoggingClass):
# Check if rtp version is 2 # Check if rtp version is 2
if rtp.version != 2: if rtp.version != 2:
self.log.debug('[%s] [VoiceData] Received an invalid RTP packet version, %s', self.vc, rtp.version) self.log.debug('[{}] [VoiceData] Received an invalid RTP packet version, {}'.format(self.vc, rtp.version))
continue continue
payload_type = RTPPayloadTypes.get(rtp.payload_type) payload_type = RTPPayloadTypes.get(rtp.payload_type)
# Unsupported payload type received # Unsupported payload type received
if not payload_type: if not payload_type:
self.log.debug('[%s] [VoiceData] Received unsupported payload type, %s', self.vc, rtp.payload_type) self.log.debug('[{}] [VoiceData] Received unsupported payload type, {}'.format(self.vc, rtp.payload_type))
continue continue
nonce = bytearray(24) nonce = bytearray(24)
@ -240,13 +240,13 @@ class UDPVoiceClient(LoggingClass):
elif self.vc.mode == 'xsalsa20_poly1305': elif self.vc.mode == 'xsalsa20_poly1305':
nonce[:12] = data[:12] nonce[:12] = data[:12]
else: else:
self.log.debug('[%s] [VoiceData] Unsupported Encryption Mode, %s', self.vc, self.vc.mode) self.log.debug('[{}] [VoiceData] Unsupported Encryption Mode, {}'.format(self.vc, self.vc.mode))
continue continue
try: try:
data = self._secret_box.decrypt(bytes(data[12:]), bytes(nonce)) data = self._secret_box.decrypt(bytes(data[12:]), bytes(nonce))
except Exception: except Exception:
self.log.debug('[%s] [VoiceData] Failed to decode data from ssrc %s', self.vc, rtp.ssrc) self.log.debug('[{}] [VoiceData] Failed to decode data from ssrc {}'.format(self.vc, rtp.ssrc))
continue continue
# RFC3550 Section 5.1 (Padding) # RFC3550 Section 5.1 (Padding)
@ -290,7 +290,7 @@ class UDPVoiceClient(LoggingClass):
# RFC3550 Section 5.3: Profile-Specific Modifications to the RTP Header # RFC3550 Section 5.3: Profile-Specific Modifications to the RTP Header
# clients send it sometimes, definitely on fresh connects to a server, dunno what to do here # clients send it sometimes, definitely on fresh connects to a server, dunno what to do here
if rtp.marker: if rtp.marker:
self.log.debug('[%s] [VoiceData] Received RTP data with the marker set, skipping', self.vc) self.log.debug('[{}] [VoiceData] Received RTP data with the marker set, skipping'.format(self.vc))
continue continue
payload = VoiceData( payload = VoiceData(
@ -328,7 +328,7 @@ class UDPVoiceClient(LoggingClass):
try: try:
data, addr = gevent.spawn(lambda: self.conn.recvfrom(70)).get(timeout=timeout) data, addr = gevent.spawn(lambda: self.conn.recvfrom(70)).get(timeout=timeout)
except gevent.Timeout: except gevent.Timeout:
return (None, None) return None, None
# Read IP and port # Read IP and port
ip = str(data[4:]).split('\x00', 1)[0] ip = str(data[4:]).split('\x00', 1)[0]
@ -338,4 +338,4 @@ class UDPVoiceClient(LoggingClass):
self.connected = True self.connected = True
self._run_task = gevent.spawn(self.run) self._run_task = gevent.spawn(self.run)
return (ip, port) return ip, port

12
docs/bot_tutorial/building_block_listeners.md

@ -1,6 +1,6 @@
# Listeners # Listeners
Listeners provide an API to listen to and execute code upon the occurance of specified Discord events. Listeners provide an API to listen to and execute code upon the occurrence of specified Discord events.
## Listener Basics ## Listener Basics
@ -9,7 +9,7 @@ To start off with, lets create a listener attached to our plugin that fires when
```py ```py
@Plugin.listen('MessageCreate') @Plugin.listen('MessageCreate')
def on_message_create(self, event): def on_message_create(self, event):
self.log.debug('Got message: %s', event.message) self.log.debug('Got message: {}'.format(event.message))
``` ```
Ok, but what if we want to make a listener which welcomes new users to our server? Well thats also easy: Ok, but what if we want to make a listener which welcomes new users to our server? Well thats also easy:
@ -28,7 +28,7 @@ To see all the events you can subscribe too, checkout the [gateway events list](
## Listener Priority ## Listener Priority
Each listener thats registered comes with a priority. This priority describes how the builtin event emitter will distribute events to your listener. To set a priority you can simply pass the priority kwarg: Each listener that's registered comes with a priority. This priority describes how the builtin event emitter will distribute events to your listener. To set a priority you can simply pass the priority kwarg:
```py ```py
from holster.emitter import Priority from holster.emitter import Priority
@ -43,6 +43,6 @@ def on_guild_member_add(self, event):
| Name | Description | | Name | Description |
|------|-------------| |------|-------------|
| BEFORE | Recieves all events sequentially alongside the emitter. This is the most dangerous priority level, as any executed code will block other events in the emitter from flowing. Blocking within a BEFORE handler can be lethal. | | BEFORE | Receives all events sequentially alongside the emitter. This is the most dangerous priority level, as any executed code will block other events in the emitter from flowing. Blocking within a BEFORE handler can be lethal. |
| SEQUENTIAL | Recieves all events sequentially, but within a seperate greenlet. This priority level can be used for plugins that require sequential events but may block or take a long time to execute their event handler. | | SEQUENTIAL | Receives all events sequentially, but within a separate greenlet. This priority level can be used for plugins that require sequential events but may block or take a long time to execute their event handler. |
| NONE | This priority provides no guarentees about the ordering of events. Similar to SEQUENTIAL all event handlers are called within a seperate greenlet. | | NONE | This priority provides no guarantees about the ordering of events. Similar to SEQUENTIAL all event handlers are called within a separate greenlet. |

8
setup.py

@ -10,16 +10,16 @@ with open('README.md') as f:
readme = f.read() readme = f.read()
extras_require = { extras_require = {
'voice': ['pynacl==1.2.1'], 'voice': ['pynacl>=1.2.1'],
'http': ['flask==0.12.2'], 'http': ['flask>=0.12.2'],
'yaml': ['pyyaml==3.12'], 'yaml': ['pyyaml>=3.12'],
'music': ['youtube_dl>=2018.1.21'], 'music': ['youtube_dl>=2018.1.21'],
'performance': [ 'performance': [
'erlpack==0.3.2' if sys.version_info.major == 2 else 'earl-etf==2.1.2', 'erlpack==0.3.2' if sys.version_info.major == 2 else 'earl-etf==2.1.2',
'ujson==1.35', 'ujson==1.35',
'wsaccel==0.6.2', 'wsaccel==0.6.2',
], ],
'sharding': ['gipc==0.6.0'], 'sharding': ['gipc>=0.6.0'],
'docs': ['biblio==0.0.4'], 'docs': ['biblio==0.0.4'],
} }

Loading…
Cancel
Save