19 changed files with 342 additions and 293 deletions
@ -1,8 +1,15 @@ |
|||
|
|||
|
|||
def cached_property(f): |
|||
def deco(self, *args, **kwargs): |
|||
def getf(self, *args, **kwargs): |
|||
if not hasattr(self, '__' + f.__name__): |
|||
setattr(self, '__' + f.__name__, f(self, *args, **kwargs)) |
|||
return getattr(self, '__' + f.__name__) |
|||
return property(deco) |
|||
|
|||
def setf(self, value): |
|||
setattr(self, '__' + f.__name__, value) |
|||
|
|||
def delf(self): |
|||
setattr(self, '__' + f.__name__, None) |
|||
|
|||
return property(getf, setf, delf) |
|||
|
@ -0,0 +1,11 @@ |
|||
from __future__ import absolute_import |
|||
|
|||
from json import dumps |
|||
|
|||
try: |
|||
from rapidjson import loads |
|||
except ImportError: |
|||
print '[WARNING] rapidjson not installed, falling back to default Python JSON parser' |
|||
from json import loads |
|||
|
|||
__all__ = ['dumps', 'loads'] |
@ -1,139 +0,0 @@ |
|||
import inspect |
|||
|
|||
|
|||
class TypedClassException(Exception): |
|||
pass |
|||
|
|||
|
|||
def construct_typed_class(cls, data): |
|||
obj = cls() |
|||
load_typed_class(obj, data) |
|||
return obj |
|||
|
|||
|
|||
def get_field_and_alias(field): |
|||
if isinstance(field, tuple): |
|||
return field |
|||
else: |
|||
return field, field |
|||
|
|||
|
|||
def get_optional(typ): |
|||
if isinstance(typ, tuple) and len(typ) == 1: |
|||
return True, typ[0] |
|||
return False, typ |
|||
|
|||
|
|||
def cast(typ, value): |
|||
valid = True |
|||
|
|||
# TODO: better exceptions |
|||
if isinstance(typ, list): |
|||
if typ: |
|||
typ = typ[0] |
|||
value = map(typ, value) |
|||
else: |
|||
list(value) |
|||
elif isinstance(typ, dict): |
|||
if typ: |
|||
ktyp, vtyp = typ.items()[0] |
|||
value = {ktyp(k): vtyp(v) for k, v in typ.items()} |
|||
else: |
|||
dict(value) |
|||
elif isinstance(typ, set): |
|||
if typ: |
|||
typ = list(typ)[0] |
|||
value = set(map(typ, value)) |
|||
else: |
|||
set(value) |
|||
elif isinstance(typ, str): |
|||
valid = False |
|||
elif not isinstance(value, typ): |
|||
value = typ(value) |
|||
|
|||
return valid, value |
|||
|
|||
|
|||
def load_typed_class(obj, params, data): |
|||
print obj, params, data |
|||
for field, typ in params.items(): |
|||
field, alias = get_field_and_alias(field) |
|||
|
|||
# Skipped field |
|||
if typ is None: |
|||
continue |
|||
|
|||
optional, typ = get_optional(typ) |
|||
if field not in data and not optional: |
|||
raise TypedClassException('Missing value for attribute `{}`'.format(field)) |
|||
|
|||
value = data[field] |
|||
|
|||
print field, alias, value, typ |
|||
if value is None: |
|||
if not optional: |
|||
raise TypedClassException('Non-optional attribute `{}` cannot take None'.format(field)) |
|||
else: |
|||
valid, value = cast(typ, value) |
|||
if not valid: |
|||
continue |
|||
|
|||
setattr(obj, alias, value) |
|||
|
|||
|
|||
def dump_typed_class(obj, params): |
|||
data = {} |
|||
|
|||
for field, typ in params.items(): |
|||
field, alias = get_field_and_alias(field) |
|||
|
|||
value = getattr(obj, alias, None) |
|||
|
|||
if typ is None: |
|||
data[field] = typ |
|||
continue |
|||
|
|||
optional, typ = get_optional(typ) |
|||
if not value and not optional: |
|||
raise TypedClassException('Missing value for attribute `{}`'.format(field)) |
|||
|
|||
_, value = cast(typ, value) |
|||
data[field] = value |
|||
|
|||
return data |
|||
|
|||
|
|||
def get_params(obj): |
|||
assert(issubclass(obj.__class__, TypedClass)) |
|||
|
|||
if not hasattr(obj.__class__, '_cached_oop_params'): |
|||
base = {} |
|||
for cls in reversed(inspect.getmro(obj.__class__)): |
|||
base.update(getattr(cls, 'PARAMS', {})) |
|||
obj.__class__._cached_oop_params = base |
|||
return obj.__class__._cached_oop_params |
|||
|
|||
|
|||
def load_typed_instance(obj, data): |
|||
return load_typed_class(obj, get_params(obj), data) |
|||
|
|||
|
|||
class TypedClass(object): |
|||
def __init__(self, **kwargs): |
|||
# TODO: validate |
|||
self.__dict__.update(kwargs) |
|||
|
|||
@classmethod |
|||
def from_dict(cls, data): |
|||
self = cls() |
|||
load_typed_instance(self, data) |
|||
return self |
|||
|
|||
def to_dict(self): |
|||
return dump_typed_class(self, get_params(self)) |
|||
|
|||
|
|||
def require_implementation(attr): |
|||
def _f(self, *args, **kwargs): |
|||
raise NotImplementedError('{} must implement method {}', self.__class__.__name, attr) |
|||
return _f |
@ -0,0 +1,28 @@ |
|||
from __future__ import absolute_import |
|||
|
|||
import websocket |
|||
import gevent |
|||
import six |
|||
|
|||
from disco.util.logging import LoggingClass |
|||
|
|||
|
|||
class Websocket(LoggingClass, websocket.WebSocketApp): |
|||
def __init__(self, *args, **kwargs): |
|||
LoggingClass.__init__(self) |
|||
websocket.WebSocketApp.__init__(self, *args, **kwargs) |
|||
def _get_close_args(self, data): |
|||
if data and len(data) >= 2: |
|||
code = 256 * six.byte2int(data[0:1]) + six.byte2int(data[1:2]) |
|||
reason = data[2:].decode('utf-8') |
|||
return [code, reason] |
|||
return [None, None] |
|||
|
|||
def _callback(self, callback, *args): |
|||
if not callback: |
|||
return |
|||
|
|||
try: |
|||
gevent.spawn(callback, self, *args) |
|||
except Exception as e: |
|||
self.log.exception('Error in Websocket callback for {}: '.format(callback)) |
@ -0,0 +1,119 @@ |
|||
import gevent |
|||
|
|||
from holster.enum import Enum |
|||
from holster.emitter import Emitter |
|||
|
|||
from disco.util.websocket import Websocket |
|||
from disco.util.logging import LoggingClass |
|||
from disco.util.json import loads, dumps |
|||
from disco.voice.packets import VoiceOPCode |
|||
from disco.gateway.packets import OPCode |
|||
|
|||
VoiceState = Enum( |
|||
DISCONNECTED=0, |
|||
AWAITING_ENDPOINT=1, |
|||
AUTHENTICATING=2, |
|||
CONNECTING=3, |
|||
CONNECTED=4, |
|||
VOICE_CONNECTING=5, |
|||
VOICE_CONNECTED=6, |
|||
) |
|||
|
|||
|
|||
class VoiceException(Exception): |
|||
def __init__(self, msg, client): |
|||
self.voice_client = client |
|||
super(VoiceException, self).__init__(msg) |
|||
|
|||
|
|||
class VoiceClient(LoggingClass): |
|||
def __init__(self, channel): |
|||
assert(channel.is_voice) |
|||
self.channel = channel |
|||
self.client = self.channel.client |
|||
|
|||
self.packets = Emitter(gevent.spawn) |
|||
self.packets.on(VoiceOPCode.READY, self.on_voice_ready) |
|||
self.packets.on(VoiceOPCode.SESSION_DESCRIPTION, self.on_voice_sdp) |
|||
|
|||
# State |
|||
self.state = VoiceState.DISCONNECTED |
|||
self.connected = gevent.event.Event() |
|||
self.token = None |
|||
self.endpoint = None |
|||
|
|||
self.update_listener = None |
|||
|
|||
# Websocket connection |
|||
self.ws = None |
|||
|
|||
def send(self, op, data): |
|||
self.ws.send(dumps({ |
|||
'op': op.value, |
|||
'd': data, |
|||
})) |
|||
|
|||
def on_voice_ready(self, data): |
|||
print data |
|||
|
|||
def on_voice_sdp(self, data): |
|||
print data |
|||
|
|||
def on_voice_server_update(self, data): |
|||
if self.channel.guild_id != data.guild_id or not data.token: |
|||
return |
|||
|
|||
if self.token and self.token != data.token: |
|||
return |
|||
|
|||
self.token = data.token |
|||
self.state = VoiceState.AUTHENTICATING |
|||
|
|||
self.endpoint = 'wss://{}'.format(data.endpoint.split(':', 1)[0]) |
|||
self.ws = Websocket( |
|||
self.endpoint, |
|||
on_message=self.on_message, |
|||
on_error=self.on_error, |
|||
on_open=self.on_open, |
|||
on_close=self.on_close, |
|||
) |
|||
self.ws.run_forever() |
|||
|
|||
def on_message(self, ws, msg): |
|||
try: |
|||
data = loads(msg) |
|||
except: |
|||
self.log.exception('Failed to parse voice gateway message: ') |
|||
|
|||
self.packets.emit(VoiceOPCode[data['op']], data) |
|||
|
|||
def on_error(self, ws, err): |
|||
# TODO |
|||
self.log.warning('Voice websocket error: {}'.format(err)) |
|||
|
|||
def on_open(self, ws): |
|||
self.send(VoiceOPCode.IDENTIFY, { |
|||
'server_id': self.channel.guild_id, |
|||
'user_id': self.client.state.me.id, |
|||
'session_id': self.client.gw.session_id, |
|||
'token': self.token |
|||
}) |
|||
|
|||
def on_close(self, ws): |
|||
# TODO |
|||
self.log.warning('Voice websocket disconnected') |
|||
|
|||
def connect(self, timeout=5, mute=False, deaf=False): |
|||
self.state = VoiceState.AWAITING_ENDPOINT |
|||
|
|||
self.update_listener = self.client.events.on('VoiceServerUpdate', self.on_voice_server_update) |
|||
|
|||
self.client.gw.send(OPCode.VOICE_STATE_UPDATE, { |
|||
'self_mute': mute, |
|||
'self_deaf': deaf, |
|||
'guild_id': int(self.channel.guild_id), |
|||
'channel_id': int(self.channel.id), |
|||
}) |
|||
|
|||
if not self.connected.wait(timeout) or self.state != VoiceState.CONNECTED: |
|||
raise VoiceException('Failed to connect to voice', self) |
@ -0,0 +1,10 @@ |
|||
from holster.enum import Enum |
|||
|
|||
VoiceOPCode = Enum( |
|||
IDENTIFY=0, |
|||
SELECT_PROTOCOL=1, |
|||
READY=2, |
|||
HEARTBEAT=3, |
|||
SESSION_DESCRIPTION=4, |
|||
SPEAKING=5, |
|||
) |
Loading…
Reference in new issue