48 changed files with 1505 additions and 496 deletions
@ -0,0 +1,13 @@ |
|||||
|
language: python |
||||
|
|
||||
|
cache: pip |
||||
|
|
||||
|
python: |
||||
|
- '2.7' |
||||
|
- '3.3' |
||||
|
- '3.4' |
||||
|
- '3.5' |
||||
|
- '3.6' |
||||
|
- 'nightly' |
||||
|
|
||||
|
script: 'python setup.py test' |
@ -1 +1 @@ |
|||||
VERSION = '0.0.7' |
VERSION = '0.0.8' |
||||
|
@ -1,15 +0,0 @@ |
|||||
import inspect |
|
||||
import importlib |
|
||||
|
|
||||
from .base import BaseProvider |
|
||||
|
|
||||
|
|
||||
def load_provider(name): |
|
||||
try: |
|
||||
mod = importlib.import_module('disco.bot.providers.' + name) |
|
||||
except ImportError: |
|
||||
mod = importlib.import_module(name) |
|
||||
|
|
||||
for entry in filter(inspect.isclass, map(lambda i: getattr(mod, i), dir(mod))): |
|
||||
if issubclass(entry, BaseProvider) and entry != BaseProvider: |
|
||||
return entry |
|
@ -1,134 +0,0 @@ |
|||||
import six |
|
||||
import pickle |
|
||||
|
|
||||
from six.moves import map, UserDict |
|
||||
|
|
||||
|
|
||||
ROOT_SENTINEL = u'\u200B' |
|
||||
SEP_SENTINEL = u'\u200D' |
|
||||
OBJ_SENTINEL = u'\u200C' |
|
||||
CAST_SENTINEL = u'\u24EA' |
|
||||
|
|
||||
|
|
||||
def join_key(*args): |
|
||||
nargs = [] |
|
||||
for arg in args: |
|
||||
if not isinstance(arg, six.string_types): |
|
||||
arg = CAST_SENTINEL + pickle.dumps(arg) |
|
||||
nargs.append(arg) |
|
||||
return SEP_SENTINEL.join(nargs) |
|
||||
|
|
||||
|
|
||||
def true_key(key): |
|
||||
key = key.rsplit(SEP_SENTINEL, 1)[-1] |
|
||||
if key.startswith(CAST_SENTINEL): |
|
||||
return pickle.loads(key) |
|
||||
return key |
|
||||
|
|
||||
|
|
||||
class BaseProvider(object): |
|
||||
def __init__(self, config): |
|
||||
self.config = config |
|
||||
self.data = {} |
|
||||
|
|
||||
def exists(self, key): |
|
||||
return key in self.data |
|
||||
|
|
||||
def keys(self, other): |
|
||||
count = other.count(SEP_SENTINEL) + 1 |
|
||||
for key in self.data.keys(): |
|
||||
if key.startswith(other) and key.count(SEP_SENTINEL) == count: |
|
||||
yield key |
|
||||
|
|
||||
def get_many(self, keys): |
|
||||
for key in keys: |
|
||||
yield key, self.get(key) |
|
||||
|
|
||||
def get(self, key): |
|
||||
return self.data[key] |
|
||||
|
|
||||
def set(self, key, value): |
|
||||
self.data[key] = value |
|
||||
|
|
||||
def delete(self, key): |
|
||||
del self.data[key] |
|
||||
|
|
||||
def load(self): |
|
||||
pass |
|
||||
|
|
||||
def save(self): |
|
||||
pass |
|
||||
|
|
||||
def root(self): |
|
||||
return StorageDict(self) |
|
||||
|
|
||||
|
|
||||
class StorageDict(UserDict): |
|
||||
def __init__(self, parent_or_provider, key=None): |
|
||||
if isinstance(parent_or_provider, BaseProvider): |
|
||||
self.provider = parent_or_provider |
|
||||
self.parent = None |
|
||||
else: |
|
||||
self.parent = parent_or_provider |
|
||||
self.provider = self.parent.provider |
|
||||
self._key = key or ROOT_SENTINEL |
|
||||
|
|
||||
def keys(self): |
|
||||
return map(true_key, self.provider.keys(self.key)) |
|
||||
|
|
||||
def values(self): |
|
||||
for key in self.keys(): |
|
||||
yield self.provider.get(key) |
|
||||
|
|
||||
def items(self): |
|
||||
for key in self.keys(): |
|
||||
yield (true_key(key), self.provider.get(key)) |
|
||||
|
|
||||
def ensure(self, key, typ=dict): |
|
||||
if key not in self: |
|
||||
self[key] = typ() |
|
||||
return self[key] |
|
||||
|
|
||||
def update(self, obj): |
|
||||
for k, v in six.iteritems(obj): |
|
||||
self[k] = v |
|
||||
|
|
||||
@property |
|
||||
def data(self): |
|
||||
obj = {} |
|
||||
|
|
||||
for raw, value in self.provider.get_many(self.provider.keys(self.key)): |
|
||||
key = true_key(raw) |
|
||||
|
|
||||
if value == OBJ_SENTINEL: |
|
||||
value = self.__class__(self, key=key).data |
|
||||
obj[key] = value |
|
||||
return obj |
|
||||
|
|
||||
@property |
|
||||
def key(self): |
|
||||
if self.parent is not None: |
|
||||
return join_key(self.parent.key, self._key) |
|
||||
return self._key |
|
||||
|
|
||||
def __setitem__(self, key, value): |
|
||||
if isinstance(value, dict): |
|
||||
obj = self.__class__(self, key) |
|
||||
obj.update(value) |
|
||||
value = OBJ_SENTINEL |
|
||||
|
|
||||
self.provider.set(join_key(self.key, key), value) |
|
||||
|
|
||||
def __getitem__(self, key): |
|
||||
res = self.provider.get(join_key(self.key, key)) |
|
||||
|
|
||||
if res == OBJ_SENTINEL: |
|
||||
return self.__class__(self, key) |
|
||||
|
|
||||
return res |
|
||||
|
|
||||
def __delitem__(self, key): |
|
||||
return self.provider.delete(join_key(self.key, key)) |
|
||||
|
|
||||
def __contains__(self, key): |
|
||||
return self.provider.exists(join_key(self.key, key)) |
|
@ -1,54 +0,0 @@ |
|||||
import os |
|
||||
import gevent |
|
||||
|
|
||||
from disco.util.serializer import Serializer |
|
||||
from .base import BaseProvider |
|
||||
|
|
||||
|
|
||||
class DiskProvider(BaseProvider): |
|
||||
def __init__(self, config): |
|
||||
super(DiskProvider, self).__init__(config) |
|
||||
self.format = config.get('format', 'pickle') |
|
||||
self.path = config.get('path', 'storage') + '.' + self.format |
|
||||
self.fsync = config.get('fsync', False) |
|
||||
self.fsync_changes = config.get('fsync_changes', 1) |
|
||||
|
|
||||
self.autosave_task = None |
|
||||
self.change_count = 0 |
|
||||
|
|
||||
def autosave_loop(self, interval): |
|
||||
while True: |
|
||||
gevent.sleep(interval) |
|
||||
self.save() |
|
||||
|
|
||||
def _on_change(self): |
|
||||
if self.fsync: |
|
||||
self.change_count += 1 |
|
||||
|
|
||||
if self.change_count >= self.fsync_changes: |
|
||||
self.save() |
|
||||
self.change_count = 0 |
|
||||
|
|
||||
def load(self): |
|
||||
if not os.path.exists(self.path): |
|
||||
return |
|
||||
|
|
||||
if self.config.get('autosave', True): |
|
||||
self.autosave_task = gevent.spawn( |
|
||||
self.autosave_loop, |
|
||||
self.config.get('autosave_interval', 120)) |
|
||||
|
|
||||
with open(self.path, 'r') as f: |
|
||||
self.data = Serializer.loads(self.format, f.read()) |
|
||||
|
|
||||
def save(self): |
|
||||
with open(self.path, 'w') as f: |
|
||||
f.write(Serializer.dumps(self.format, self.data)) |
|
||||
|
|
||||
def set(self, key, value): |
|
||||
super(DiskProvider, self).set(key, value) |
|
||||
self._on_change() |
|
||||
|
|
||||
def delete(self, key): |
|
||||
super(DiskProvider, self).delete(key) |
|
||||
self._on_change() |
|
@ -1,5 +0,0 @@ |
|||||
from .base import BaseProvider |
|
||||
|
|
||||
|
|
||||
class MemoryProvider(BaseProvider): |
|
||||
pass |
|
@ -1,48 +0,0 @@ |
|||||
from __future__ import absolute_import |
|
||||
|
|
||||
import redis |
|
||||
|
|
||||
from itertools import izip |
|
||||
|
|
||||
from disco.util.serializer import Serializer |
|
||||
from .base import BaseProvider, SEP_SENTINEL |
|
||||
|
|
||||
|
|
||||
class RedisProvider(BaseProvider): |
|
||||
def __init__(self, config): |
|
||||
super(RedisProvider, self).__init__(config) |
|
||||
self.format = config.get('format', 'pickle') |
|
||||
self.conn = None |
|
||||
|
|
||||
def load(self): |
|
||||
self.conn = redis.Redis( |
|
||||
host=self.config.get('host', 'localhost'), |
|
||||
port=self.config.get('port', 6379), |
|
||||
db=self.config.get('db', 0)) |
|
||||
|
|
||||
def exists(self, key): |
|
||||
return self.conn.exists(key) |
|
||||
|
|
||||
def keys(self, other): |
|
||||
count = other.count(SEP_SENTINEL) + 1 |
|
||||
for key in self.conn.scan_iter(u'{}*'.format(other)): |
|
||||
key = key.decode('utf-8') |
|
||||
if key.count(SEP_SENTINEL) == count: |
|
||||
yield key |
|
||||
|
|
||||
def get_many(self, keys): |
|
||||
keys = list(keys) |
|
||||
if not len(keys): |
|
||||
raise StopIteration |
|
||||
|
|
||||
for key, value in izip(keys, self.conn.mget(keys)): |
|
||||
yield (key, Serializer.loads(self.format, value)) |
|
||||
|
|
||||
def get(self, key): |
|
||||
return Serializer.loads(self.format, self.conn.get(key)) |
|
||||
|
|
||||
def set(self, key, value): |
|
||||
self.conn.set(key, Serializer.dumps(self.format, value)) |
|
||||
|
|
||||
def delete(self, key): |
|
||||
self.conn.delete(key) |
|
@ -1,52 +0,0 @@ |
|||||
from __future__ import absolute_import |
|
||||
|
|
||||
import six |
|
||||
import rocksdb |
|
||||
|
|
||||
from itertools import izip |
|
||||
from six.moves import map |
|
||||
|
|
||||
from disco.util.serializer import Serializer |
|
||||
from .base import BaseProvider, SEP_SENTINEL |
|
||||
|
|
||||
|
|
||||
class RocksDBProvider(BaseProvider): |
|
||||
def __init__(self, config): |
|
||||
super(RocksDBProvider, self).__init__(config) |
|
||||
self.format = config.get('format', 'pickle') |
|
||||
self.path = config.get('path', 'storage.db') |
|
||||
self.db = None |
|
||||
|
|
||||
@staticmethod |
|
||||
def k(k): |
|
||||
return bytes(k) if six.PY3 else str(k.encode('utf-8')) |
|
||||
|
|
||||
def load(self): |
|
||||
self.db = rocksdb.DB(self.path, rocksdb.Options(create_if_missing=True)) |
|
||||
|
|
||||
def exists(self, key): |
|
||||
return self.db.get(self.k(key)) is not None |
|
||||
|
|
||||
# TODO prefix extractor |
|
||||
def keys(self, other): |
|
||||
count = other.count(SEP_SENTINEL) + 1 |
|
||||
it = self.db.iterkeys() |
|
||||
it.seek_to_first() |
|
||||
|
|
||||
for key in it: |
|
||||
key = key.decode('utf-8') |
|
||||
if key.startswith(other) and key.count(SEP_SENTINEL) == count: |
|
||||
yield key |
|
||||
|
|
||||
def get_many(self, keys): |
|
||||
for key, value in izip(keys, self.db.multi_get(list(map(self.k, keys)))): |
|
||||
yield (key, Serializer.loads(self.format, value.decode('utf-8'))) |
|
||||
|
|
||||
def get(self, key): |
|
||||
return Serializer.loads(self.format, self.db.get(self.k(key)).decode('utf-8')) |
|
||||
|
|
||||
def set(self, key, value): |
|
||||
self.db.put(self.k(key), Serializer.dumps(self.format, value)) |
|
||||
|
|
||||
def delete(self, key): |
|
||||
self.db.delete(self.k(key)) |
|
@ -1,26 +1,87 @@ |
|||||
from .providers import load_provider |
import os |
||||
|
|
||||
|
from six.moves import UserDict |
||||
|
|
||||
class Storage(object): |
from disco.util.hashmap import HashMap |
||||
def __init__(self, ctx, config): |
from disco.util.serializer import Serializer |
||||
|
|
||||
|
|
||||
|
class StorageHashMap(HashMap): |
||||
|
def __init__(self, data): |
||||
|
self.data = data |
||||
|
|
||||
|
|
||||
|
class ContextAwareProxy(UserDict): |
||||
|
def __init__(self, ctx): |
||||
self.ctx = ctx |
self.ctx = ctx |
||||
self.config = config |
|
||||
self.provider = load_provider(config.provider)(config.config) |
|
||||
self.provider.load() |
|
||||
self.root = self.provider.root() |
|
||||
|
|
||||
@property |
@property |
||||
def plugin(self): |
def data(self): |
||||
return self.root.ensure('plugins').ensure(self.ctx['plugin'].name) |
return self.ctx() |
||||
|
|
||||
@property |
|
||||
def guild(self): |
|
||||
return self.plugin.ensure('guilds').ensure(self.ctx['guild'].id) |
|
||||
|
|
||||
@property |
class StorageDict(UserDict): |
||||
def channel(self): |
def __init__(self, parent, data): |
||||
return self.plugin.ensure('channels').ensure(self.ctx['channel'].id) |
self._parent = parent |
||||
|
self.data = data |
||||
|
|
||||
@property |
def update(self, other): |
||||
def user(self): |
self.data.update(other) |
||||
return self.plugin.ensure('users').ensure(self.ctx['user'].id) |
self._parent._update() |
||||
|
|
||||
|
def __setitem__(self, key, value): |
||||
|
self.data[key] = value |
||||
|
self._parent._update() |
||||
|
|
||||
|
def __delitem__(self, key): |
||||
|
del self.data[key] |
||||
|
self._parent._update() |
||||
|
|
||||
|
|
||||
|
class Storage(object): |
||||
|
def __init__(self, ctx, config): |
||||
|
self._ctx = ctx |
||||
|
self._path = config.path |
||||
|
self._serializer = config.serializer |
||||
|
self._fsync = config.fsync |
||||
|
self._data = {} |
||||
|
|
||||
|
if os.path.exists(self._path): |
||||
|
with open(self._path, 'r') as f: |
||||
|
self._data = Serializer.loads(self._serializer, f.read()) |
||||
|
|
||||
|
def __getitem__(self, key): |
||||
|
if key not in self._data: |
||||
|
self._data[key] = {} |
||||
|
return StorageHashMap(StorageDict(self, self._data[key])) |
||||
|
|
||||
|
def _update(self): |
||||
|
if self._fsync: |
||||
|
self.save() |
||||
|
|
||||
|
def save(self): |
||||
|
if not self._path: |
||||
|
return |
||||
|
|
||||
|
with open(self._path, 'w') as f: |
||||
|
f.write(Serializer.dumps(self._serializer, self._data)) |
||||
|
|
||||
|
def guild(self, key): |
||||
|
return ContextAwareProxy( |
||||
|
lambda: self['_g{}:{}'.format(self._ctx['guild'].id, key)] |
||||
|
) |
||||
|
|
||||
|
def channel(self, key): |
||||
|
return ContextAwareProxy( |
||||
|
lambda: self['_c{}:{}'.format(self._ctx['channel'].id, key)] |
||||
|
) |
||||
|
|
||||
|
def plugin(self, key): |
||||
|
return ContextAwareProxy( |
||||
|
lambda: self['_p{}:{}'.format(self._ctx['plugin'].name, key)] |
||||
|
) |
||||
|
|
||||
|
def user(self, key): |
||||
|
return ContextAwareProxy( |
||||
|
lambda: self['_u{}:{}'.format(self._ctx['user'].id, key)] |
||||
|
) |
||||
|
@ -0,0 +1,71 @@ |
|||||
|
import gevent |
||||
|
|
||||
|
""" |
||||
|
Object.chain -> creates a chain where each action happens after the last |
||||
|
pass_result = False -> whether the result of the last action is passed, or the original |
||||
|
|
||||
|
Object.async_chain -> creates an async chain where each action happens at the same time |
||||
|
""" |
||||
|
|
||||
|
|
||||
|
class Chainable(object): |
||||
|
__slots__ = [] |
||||
|
|
||||
|
def chain(self, pass_result=True): |
||||
|
return Chain(self, pass_result=pass_result, async_=False) |
||||
|
|
||||
|
def async_chain(self): |
||||
|
return Chain(self, pass_result=False, async_=True) |
||||
|
|
||||
|
|
||||
|
class Chain(object): |
||||
|
def __init__(self, obj, pass_result=True, async_=False): |
||||
|
self._obj = obj |
||||
|
self._pass_result = pass_result |
||||
|
self._async = async_ |
||||
|
self._parts = [] |
||||
|
|
||||
|
@property |
||||
|
def obj(self): |
||||
|
if isinstance(self._obj, Chain): |
||||
|
return self._obj._next() |
||||
|
return self._obj |
||||
|
|
||||
|
def __getattr__(self, item): |
||||
|
func = getattr(self.obj, item) |
||||
|
if not func or not callable(func): |
||||
|
return func |
||||
|
|
||||
|
def _wrapped(*args, **kwargs): |
||||
|
inst = gevent.spawn(func, *args, **kwargs) |
||||
|
self._parts.append(inst) |
||||
|
|
||||
|
# If async, just return instantly |
||||
|
if self._async: |
||||
|
return self |
||||
|
|
||||
|
# Otherwise return a chain |
||||
|
return Chain(self) |
||||
|
return _wrapped |
||||
|
|
||||
|
def _next(self): |
||||
|
res = self._parts[0].get() |
||||
|
if self._pass_result: |
||||
|
return res |
||||
|
return self |
||||
|
|
||||
|
def then(self, func, *args, **kwargs): |
||||
|
inst = gevent.spawn(func, *args, **kwargs) |
||||
|
self._parts.append(inst) |
||||
|
if self._async: |
||||
|
return self |
||||
|
return Chain(self) |
||||
|
|
||||
|
def first(self): |
||||
|
return self._obj |
||||
|
|
||||
|
def get(self, timeout=None): |
||||
|
return gevent.wait(self._parts, timeout=timeout) |
||||
|
|
||||
|
def wait(self, timeout=None): |
||||
|
gevent.joinall(self._parts, timeout=None) |
@ -0,0 +1,32 @@ |
|||||
|
import re |
||||
|
|
||||
|
|
||||
|
# Zero width (non-rendering) space that can be used to escape mentions |
||||
|
ZERO_WIDTH_SPACE = u'\u200B' |
||||
|
|
||||
|
# A grave-looking character that can be used to escape codeblocks |
||||
|
MODIFIER_GRAVE_ACCENT = u'\u02CB' |
||||
|
|
||||
|
# Regex which matches all possible mention combinations, this may be over-zealous |
||||
|
# but its better safe than sorry. |
||||
|
MENTION_RE = re.compile('<?([@|#][!|&]?[0-9]+|@everyone|@here)>?') |
||||
|
|
||||
|
|
||||
|
def _re_sub_mention(mention): |
||||
|
mention = mention.group(1) |
||||
|
if '#' in mention: |
||||
|
return (u'#' + ZERO_WIDTH_SPACE).join(mention.split('#', 1)) |
||||
|
elif '@' in mention: |
||||
|
return (u'@' + ZERO_WIDTH_SPACE).join(mention.split('@', 1)) |
||||
|
else: |
||||
|
return mention |
||||
|
|
||||
|
|
||||
|
def S(text, escape_mentions=True, escape_codeblocks=False): |
||||
|
if escape_mentions: |
||||
|
text = MENTION_RE.sub(_re_sub_mention, text) |
||||
|
|
||||
|
if escape_codeblocks: |
||||
|
text = text.replace('`', MODIFIER_GRAVE_ACCENT) |
||||
|
|
||||
|
return text |
@ -0,0 +1,3 @@ |
|||||
|
from disco.voice.client import * |
||||
|
from disco.voice.player import * |
||||
|
from disco.voice.playable import * |
@ -0,0 +1,149 @@ |
|||||
|
import sys |
||||
|
import array |
||||
|
import ctypes |
||||
|
import ctypes.util |
||||
|
|
||||
|
from holster.enum import Enum |
||||
|
|
||||
|
from disco.util.logging import LoggingClass |
||||
|
|
||||
|
|
||||
|
c_int_ptr = ctypes.POINTER(ctypes.c_int) |
||||
|
c_int16_ptr = ctypes.POINTER(ctypes.c_int16) |
||||
|
c_float_ptr = ctypes.POINTER(ctypes.c_float) |
||||
|
|
||||
|
|
||||
|
class EncoderStruct(ctypes.Structure): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
class DecoderStruct(ctypes.Structure): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
EncoderStructPtr = ctypes.POINTER(EncoderStruct) |
||||
|
DecoderStructPtr = ctypes.POINTER(DecoderStruct) |
||||
|
|
||||
|
|
||||
|
class BaseOpus(LoggingClass): |
||||
|
BASE_EXPORTED = { |
||||
|
'opus_strerror': ([ctypes.c_int], ctypes.c_char_p), |
||||
|
} |
||||
|
|
||||
|
EXPORTED = {} |
||||
|
|
||||
|
def __init__(self, library_path=None): |
||||
|
self.path = library_path or self.find_library() |
||||
|
self.lib = ctypes.cdll.LoadLibrary(self.path) |
||||
|
|
||||
|
methods = {} |
||||
|
methods.update(self.BASE_EXPORTED) |
||||
|
methods.update(self.EXPORTED) |
||||
|
|
||||
|
for name, item in methods.items(): |
||||
|
func = getattr(self.lib, name) |
||||
|
|
||||
|
if item[0]: |
||||
|
func.argtypes = item[0] |
||||
|
|
||||
|
func.restype = item[1] |
||||
|
|
||||
|
setattr(self, name, func) |
||||
|
|
||||
|
@staticmethod |
||||
|
def find_library(): |
||||
|
if sys.platform == 'win32': |
||||
|
raise Exception('Cannot auto-load opus on Windows, please specify full library path') |
||||
|
|
||||
|
return ctypes.util.find_library('opus') |
||||
|
|
||||
|
|
||||
|
Application = Enum( |
||||
|
AUDIO=2049, |
||||
|
VOIP=2048, |
||||
|
LOWDELAY=2051 |
||||
|
) |
||||
|
|
||||
|
|
||||
|
Control = Enum( |
||||
|
SET_BITRATE=4002, |
||||
|
SET_BANDWIDTH=4008, |
||||
|
SET_FEC=4012, |
||||
|
SET_PLP=4014, |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class OpusEncoder(BaseOpus): |
||||
|
EXPORTED = { |
||||
|
'opus_encoder_get_size': ([ctypes.c_int], ctypes.c_int), |
||||
|
'opus_encoder_create': ([ctypes.c_int, ctypes.c_int, ctypes.c_int, c_int_ptr], EncoderStructPtr), |
||||
|
'opus_encode': ([EncoderStructPtr, c_int16_ptr, ctypes.c_int, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int32), |
||||
|
'opus_encoder_ctl': (None, ctypes.c_int32), |
||||
|
'opus_encoder_destroy': ([EncoderStructPtr], None), |
||||
|
} |
||||
|
|
||||
|
def __init__(self, sampling_rate, channels, application=Application.AUDIO, library_path=None): |
||||
|
super(OpusEncoder, self).__init__(library_path) |
||||
|
self.sampling_rate = sampling_rate |
||||
|
self.channels = channels |
||||
|
self.application = application |
||||
|
|
||||
|
self._inst = None |
||||
|
|
||||
|
@property |
||||
|
def inst(self): |
||||
|
if not self._inst: |
||||
|
self._inst = self.create() |
||||
|
self.set_bitrate(128) |
||||
|
self.set_fec(True) |
||||
|
self.set_expected_packet_loss_percent(0.15) |
||||
|
return self._inst |
||||
|
|
||||
|
def set_bitrate(self, kbps): |
||||
|
kbps = min(128, max(16, int(kbps))) |
||||
|
ret = self.opus_encoder_ctl(self.inst, int(Control.SET_BITRATE), kbps * 1024) |
||||
|
|
||||
|
if ret < 0: |
||||
|
raise Exception('Failed to set bitrate to {}: {}'.format(kbps, ret)) |
||||
|
|
||||
|
def set_fec(self, value): |
||||
|
ret = self.opus_encoder_ctl(self.inst, int(Control.SET_FEC), int(value)) |
||||
|
|
||||
|
if ret < 0: |
||||
|
raise Exception('Failed to set FEC to {}: {}'.format(value, ret)) |
||||
|
|
||||
|
def set_expected_packet_loss_percent(self, perc): |
||||
|
ret = self.opus_encoder_ctl(self.inst, int(Control.SET_PLP), min(100, max(0, int(perc * 100)))) |
||||
|
|
||||
|
if ret < 0: |
||||
|
raise Exception('Failed to set PLP to {}: {}'.format(perc, ret)) |
||||
|
|
||||
|
def create(self): |
||||
|
ret = ctypes.c_int() |
||||
|
result = self.opus_encoder_create(self.sampling_rate, self.channels, self.application.value, ctypes.byref(ret)) |
||||
|
|
||||
|
if ret.value != 0: |
||||
|
raise Exception('Failed to create opus encoder: {}'.format(ret.value)) |
||||
|
|
||||
|
return result |
||||
|
|
||||
|
def __del__(self): |
||||
|
if hasattr(self, '_inst') and self._inst: |
||||
|
self.opus_encoder_destroy(self._inst) |
||||
|
self._inst = None |
||||
|
|
||||
|
def encode(self, pcm, frame_size): |
||||
|
max_data_bytes = len(pcm) |
||||
|
pcm = ctypes.cast(pcm, c_int16_ptr) |
||||
|
data = (ctypes.c_char * max_data_bytes)() |
||||
|
|
||||
|
ret = self.opus_encode(self.inst, pcm, frame_size, data, max_data_bytes) |
||||
|
if ret < 0: |
||||
|
raise Exception('Failed to encode: {}'.format(ret)) |
||||
|
|
||||
|
# TODO: py3 |
||||
|
return array.array('b', data[:ret]).tostring() |
||||
|
|
||||
|
|
||||
|
class OpusDecoder(BaseOpus): |
||||
|
pass |
@ -0,0 +1,354 @@ |
|||||
|
import abc |
||||
|
import six |
||||
|
import types |
||||
|
import gevent |
||||
|
import struct |
||||
|
import subprocess |
||||
|
|
||||
|
from gevent.lock import Semaphore |
||||
|
from gevent.queue import Queue |
||||
|
|
||||
|
from disco.voice.opus import OpusEncoder |
||||
|
|
||||
|
|
||||
|
try: |
||||
|
from cStringIO import cStringIO as BufferedIO |
||||
|
except: |
||||
|
if six.PY2: |
||||
|
from StringIO import StringIO as BufferedIO |
||||
|
else: |
||||
|
from io import BytesIO as BufferedIO |
||||
|
|
||||
|
|
||||
|
OPUS_HEADER_SIZE = struct.calcsize('<h') |
||||
|
|
||||
|
|
||||
|
class AbstractOpus(object): |
||||
|
def __init__(self, sampling_rate=48000, frame_length=20, channels=2): |
||||
|
self.sampling_rate = sampling_rate |
||||
|
self.frame_length = frame_length |
||||
|
self.channels = 2 |
||||
|
self.sample_size = 2 * self.channels |
||||
|
self.samples_per_frame = int(self.sampling_rate / 1000 * self.frame_length) |
||||
|
self.frame_size = self.samples_per_frame * self.sample_size |
||||
|
|
||||
|
|
||||
|
class BaseUtil(object): |
||||
|
def pipe(self, other, *args, **kwargs): |
||||
|
child = other(self, *args, **kwargs) |
||||
|
setattr(child, 'metadata', self.metadata) |
||||
|
setattr(child, '_parent', self) |
||||
|
return child |
||||
|
|
||||
|
@property |
||||
|
def metadata(self): |
||||
|
return getattr(self, '_metadata', None) |
||||
|
|
||||
|
@metadata.setter |
||||
|
def metadata(self, value): |
||||
|
self._metadata = value |
||||
|
|
||||
|
|
||||
|
@six.add_metaclass(abc.ABCMeta) |
||||
|
class BasePlayable(BaseUtil): |
||||
|
@abc.abstractmethod |
||||
|
def next_frame(self): |
||||
|
raise NotImplementedError |
||||
|
|
||||
|
|
||||
|
@six.add_metaclass(abc.ABCMeta) |
||||
|
class BaseInput(BaseUtil): |
||||
|
@abc.abstractmethod |
||||
|
def read(self, size): |
||||
|
raise NotImplementedError |
||||
|
|
||||
|
@abc.abstractmethod |
||||
|
def fileobj(self): |
||||
|
raise NotImplementedError |
||||
|
|
||||
|
|
||||
|
class OpusFilePlayable(BasePlayable, AbstractOpus): |
||||
|
""" |
||||
|
An input which reads opus data from a file or file-like object. |
||||
|
""" |
||||
|
def __init__(self, fobj, *args, **kwargs): |
||||
|
super(OpusFilePlayable, self).__init__(*args, **kwargs) |
||||
|
self.fobj = fobj |
||||
|
self.done = False |
||||
|
|
||||
|
def next_frame(self): |
||||
|
if self.done: |
||||
|
return None |
||||
|
|
||||
|
header = self.fobj.read(OPUS_HEADER_SIZE) |
||||
|
if len(header) < OPUS_HEADER_SIZE: |
||||
|
self.done = True |
||||
|
return None |
||||
|
|
||||
|
data_size = struct.unpack('<h', header)[0] |
||||
|
data = self.fobj.read(data_size) |
||||
|
if len(data) < data_size: |
||||
|
self.done = True |
||||
|
return None |
||||
|
|
||||
|
return data |
||||
|
|
||||
|
|
||||
|
class FFmpegInput(BaseInput, AbstractOpus): |
||||
|
def __init__(self, source='-', command='avconv', streaming=False, **kwargs): |
||||
|
super(FFmpegInput, self).__init__(**kwargs) |
||||
|
if source: |
||||
|
self.source = source |
||||
|
self.streaming = streaming |
||||
|
self.command = command |
||||
|
|
||||
|
self._buffer = None |
||||
|
self._proc = None |
||||
|
|
||||
|
def read(self, sz): |
||||
|
if self.streaming: |
||||
|
raise TypeError('Cannot read from a streaming FFmpegInput') |
||||
|
|
||||
|
# First read blocks until the subprocess finishes |
||||
|
if not self._buffer: |
||||
|
data, _ = self.proc.communicate() |
||||
|
self._buffer = BufferedIO(data) |
||||
|
|
||||
|
# Subsequent reads can just do dis thang |
||||
|
return self._buffer.read(sz) |
||||
|
|
||||
|
def fileobj(self): |
||||
|
if self.streaming: |
||||
|
return self.proc.stdout |
||||
|
else: |
||||
|
return self |
||||
|
|
||||
|
@property |
||||
|
def proc(self): |
||||
|
if not self._proc: |
||||
|
if callable(self.source): |
||||
|
self.source = self.source(self) |
||||
|
|
||||
|
if isinstance(self.source, (tuple, list)): |
||||
|
self.source, self.metadata = self.source |
||||
|
|
||||
|
args = [ |
||||
|
self.command, |
||||
|
'-i', str(self.source), |
||||
|
'-f', 's16le', |
||||
|
'-ar', str(self.sampling_rate), |
||||
|
'-ac', str(self.channels), |
||||
|
'-loglevel', 'warning', |
||||
|
'pipe:1' |
||||
|
] |
||||
|
self._proc = subprocess.Popen(args, stdin=None, stdout=subprocess.PIPE) |
||||
|
return self._proc |
||||
|
|
||||
|
|
||||
|
class YoutubeDLInput(FFmpegInput): |
||||
|
def __init__(self, url=None, ie_info=None, *args, **kwargs): |
||||
|
super(YoutubeDLInput, self).__init__(None, *args, **kwargs) |
||||
|
self._url = url |
||||
|
self._ie_info = ie_info |
||||
|
self._info = None |
||||
|
self._info_lock = Semaphore() |
||||
|
|
||||
|
@property |
||||
|
def info(self): |
||||
|
with self._info_lock: |
||||
|
if not self._info: |
||||
|
import youtube_dl |
||||
|
ydl = youtube_dl.YoutubeDL({'format': 'webm[abr>0]/bestaudio/best'}) |
||||
|
|
||||
|
if self._url: |
||||
|
obj = ydl.extract_info(self._url, download=False, process=False) |
||||
|
if 'entries' in obj: |
||||
|
self._ie_info = obj['entries'][0] |
||||
|
else: |
||||
|
self._ie_info = obj |
||||
|
|
||||
|
self._info = ydl.process_ie_result(self._ie_info, download=False) |
||||
|
return self._info |
||||
|
|
||||
|
@property |
||||
|
def _metadata(self): |
||||
|
return self.info |
||||
|
|
||||
|
@classmethod |
||||
|
def many(cls, url, *args, **kwargs): |
||||
|
import youtube_dl |
||||
|
|
||||
|
ydl = youtube_dl.YoutubeDL({'format': 'webm[abr>0]/bestaudio/best'}) |
||||
|
info = ydl.extract_info(url, download=False, process=False) |
||||
|
|
||||
|
if 'entries' not in info: |
||||
|
yield cls(ie_info=info, *args, **kwargs) |
||||
|
raise StopIteration |
||||
|
|
||||
|
for item in info['entries']: |
||||
|
yield cls(ie_info=item, *args, **kwargs) |
||||
|
|
||||
|
@property |
||||
|
def source(self): |
||||
|
return self.info['url'] |
||||
|
|
||||
|
|
||||
|
class BufferedOpusEncoderPlayable(BasePlayable, OpusEncoder, AbstractOpus): |
||||
|
def __init__(self, source, *args, **kwargs): |
||||
|
self.source = source |
||||
|
self.frames = Queue(kwargs.pop('queue_size', 4096)) |
||||
|
|
||||
|
# Call the AbstractOpus constructor, as we need properties it sets |
||||
|
AbstractOpus.__init__(self, *args, **kwargs) |
||||
|
|
||||
|
# Then call the OpusEncoder constructor, which requires some properties |
||||
|
# that AbstractOpus sets up |
||||
|
OpusEncoder.__init__(self, self.sampling_rate, self.channels) |
||||
|
|
||||
|
# Spawn the encoder loop |
||||
|
gevent.spawn(self._encoder_loop) |
||||
|
|
||||
|
def _encoder_loop(self): |
||||
|
while self.source: |
||||
|
raw = self.source.read(self.frame_size) |
||||
|
if len(raw) < self.frame_size: |
||||
|
break |
||||
|
|
||||
|
self.frames.put(self.encode(raw, self.samples_per_frame)) |
||||
|
gevent.idle() |
||||
|
self.source = None |
||||
|
self.frames.put(None) |
||||
|
|
||||
|
def next_frame(self): |
||||
|
return self.frames.get() |
||||
|
|
||||
|
|
||||
|
class DCADOpusEncoderPlayable(BasePlayable, AbstractOpus, OpusEncoder): |
||||
|
def __init__(self, source, *args, **kwargs): |
||||
|
self.source = source |
||||
|
self.command = kwargs.pop('command', 'dcad') |
||||
|
super(DCADOpusEncoderPlayable, self).__init__(*args, **kwargs) |
||||
|
|
||||
|
self._done = False |
||||
|
self._proc = None |
||||
|
|
||||
|
@property |
||||
|
def proc(self): |
||||
|
if not self._proc: |
||||
|
source = obj = self.source.fileobj() |
||||
|
if not hasattr(obj, 'fileno'): |
||||
|
source = subprocess.PIPE |
||||
|
|
||||
|
self._proc = subprocess.Popen([ |
||||
|
self.command, |
||||
|
'--channels', str(self.channels), |
||||
|
'--rate', str(self.sampling_rate), |
||||
|
'--size', str(self.samples_per_frame), |
||||
|
'--bitrate', '128', |
||||
|
'--fec', |
||||
|
'--packet-loss-percent', '30', |
||||
|
'--input', 'pipe:0', |
||||
|
'--output', 'pipe:1', |
||||
|
], stdin=source, stdout=subprocess.PIPE) |
||||
|
|
||||
|
def writer(): |
||||
|
while True: |
||||
|
data = obj.read(2048) |
||||
|
if len(data) > 0: |
||||
|
self._proc.stdin.write(data) |
||||
|
if len(data) < 2048: |
||||
|
break |
||||
|
|
||||
|
if source == subprocess.PIPE: |
||||
|
gevent.spawn(writer) |
||||
|
return self._proc |
||||
|
|
||||
|
def next_frame(self): |
||||
|
if self._done: |
||||
|
return None |
||||
|
|
||||
|
header = self.proc.stdout.read(OPUS_HEADER_SIZE) |
||||
|
if len(header) < OPUS_HEADER_SIZE: |
||||
|
self._done = True |
||||
|
return |
||||
|
|
||||
|
size = struct.unpack('<h', header)[0] |
||||
|
|
||||
|
data = self.proc.stdout.read(size) |
||||
|
if len(data) < size: |
||||
|
self._done = True |
||||
|
return |
||||
|
|
||||
|
return data |
||||
|
|
||||
|
|
||||
|
class FileProxyPlayable(BasePlayable, AbstractOpus): |
||||
|
def __init__(self, other, output, *args, **kwargs): |
||||
|
self.flush = kwargs.pop('flush', False) |
||||
|
self.on_complete = kwargs.pop('on_complete', None) |
||||
|
super(FileProxyPlayable, self).__init__(*args, **kwargs) |
||||
|
self.other = other |
||||
|
self.output = output |
||||
|
|
||||
|
def next_frame(self): |
||||
|
frame = self.other.next_frame() |
||||
|
|
||||
|
if frame: |
||||
|
self.output.write(struct.pack('<h', len(frame))) |
||||
|
self.output.write(frame) |
||||
|
|
||||
|
if self.flush: |
||||
|
self.output.flush() |
||||
|
else: |
||||
|
self.output.flush() |
||||
|
self.on_complete() |
||||
|
self.output.close() |
||||
|
return frame |
||||
|
|
||||
|
|
||||
|
class PlaylistPlayable(BasePlayable, AbstractOpus): |
||||
|
def __init__(self, items, *args, **kwargs): |
||||
|
super(PlaylistPlayable, self).__init__(*args, **kwargs) |
||||
|
self.items = items |
||||
|
self.now_playing = None |
||||
|
|
||||
|
def _get_next(self): |
||||
|
if isinstance(self.items, types.GeneratorType): |
||||
|
return next(self.items, None) |
||||
|
return self.items.pop() |
||||
|
|
||||
|
def next_frame(self): |
||||
|
if not self.items: |
||||
|
return |
||||
|
|
||||
|
if not self.now_playing: |
||||
|
self.now_playing = self._get_next() |
||||
|
if not self.now_playing: |
||||
|
return |
||||
|
|
||||
|
frame = self.now_playing.next_frame() |
||||
|
if not frame: |
||||
|
return self.next_frame() |
||||
|
|
||||
|
return frame |
||||
|
|
||||
|
|
||||
|
class MemoryBufferedPlayable(BasePlayable, AbstractOpus): |
||||
|
def __init__(self, other, *args, **kwargs): |
||||
|
from gevent.queue import Queue |
||||
|
|
||||
|
super(MemoryBufferedPlayable, self).__init__(*args, **kwargs) |
||||
|
self.frames = Queue() |
||||
|
self.other = other |
||||
|
gevent.spawn(self._buffer) |
||||
|
|
||||
|
def _buffer(self): |
||||
|
while True: |
||||
|
frame = self.other.next_frame() |
||||
|
if not frame: |
||||
|
break |
||||
|
self.frames.put(frame) |
||||
|
self.frames.put(None) |
||||
|
|
||||
|
def next_frame(self): |
||||
|
return self.frames.get() |
@ -0,0 +1,126 @@ |
|||||
|
import time |
||||
|
import gevent |
||||
|
|
||||
|
from six.moves import queue |
||||
|
from holster.enum import Enum |
||||
|
from holster.emitter import Emitter |
||||
|
|
||||
|
from disco.voice.client import VoiceState |
||||
|
|
||||
|
MAX_TIMESTAMP = 4294967295 |
||||
|
|
||||
|
|
||||
|
class Player(object): |
||||
|
Events = Enum( |
||||
|
'START_PLAY', |
||||
|
'STOP_PLAY', |
||||
|
'PAUSE_PLAY', |
||||
|
'RESUME_PLAY', |
||||
|
'DISCONNECT' |
||||
|
) |
||||
|
|
||||
|
def __init__(self, client): |
||||
|
self.client = client |
||||
|
|
||||
|
# Queue contains playable items |
||||
|
self.queue = queue.Queue() |
||||
|
|
||||
|
# Whether we're playing music (true for lifetime) |
||||
|
self.playing = True |
||||
|
|
||||
|
# Set to an event when playback is paused |
||||
|
self.paused = None |
||||
|
|
||||
|
# Current playing item |
||||
|
self.now_playing = None |
||||
|
|
||||
|
# Current play task |
||||
|
self.play_task = None |
||||
|
|
||||
|
# Core task |
||||
|
self.run_task = gevent.spawn(self.run) |
||||
|
|
||||
|
# Event triggered when playback is complete |
||||
|
self.complete = gevent.event.Event() |
||||
|
|
||||
|
# Event emitter for metadata |
||||
|
self.events = Emitter(gevent.spawn) |
||||
|
|
||||
|
def disconnect(self): |
||||
|
self.client.disconnect() |
||||
|
self.events.emit(self.Events.DISCONNECT) |
||||
|
|
||||
|
def skip(self): |
||||
|
self.play_task.kill() |
||||
|
|
||||
|
def pause(self): |
||||
|
if self.paused: |
||||
|
return |
||||
|
self.paused = gevent.event.Event() |
||||
|
self.events.emit(self.Events.PAUSE_PLAY) |
||||
|
|
||||
|
def resume(self): |
||||
|
self.paused.set() |
||||
|
self.paused = None |
||||
|
self.events.emit(self.Events.RESUME_PLAY) |
||||
|
|
||||
|
def play(self, item): |
||||
|
# Grab the first frame before we start anything else, sometimes playables |
||||
|
# can do some lengthy async tasks here to setup the playable and we |
||||
|
# don't want that lerp the first N frames of the playable into playing |
||||
|
# faster |
||||
|
frame = item.next_frame() |
||||
|
if frame is None: |
||||
|
return |
||||
|
|
||||
|
start = time.time() |
||||
|
loops = 0 |
||||
|
|
||||
|
while True: |
||||
|
loops += 1 |
||||
|
|
||||
|
if self.paused: |
||||
|
self.client.set_speaking(False) |
||||
|
self.paused.wait() |
||||
|
gevent.sleep(2) |
||||
|
self.client.set_speaking(True) |
||||
|
start = time.time() |
||||
|
loops = 0 |
||||
|
|
||||
|
if self.client.state == VoiceState.DISCONNECTED: |
||||
|
return |
||||
|
|
||||
|
if self.client.state != VoiceState.CONNECTED: |
||||
|
self.client.state_emitter.wait(VoiceState.CONNECTED) |
||||
|
|
||||
|
self.client.send_frame(frame) |
||||
|
self.client.timestamp += item.samples_per_frame |
||||
|
if self.client.timestamp > MAX_TIMESTAMP: |
||||
|
self.client.timestamp = 0 |
||||
|
|
||||
|
frame = item.next_frame() |
||||
|
if frame is None: |
||||
|
return |
||||
|
|
||||
|
next_time = start + 0.02 * loops |
||||
|
delay = max(0, 0.02 + (next_time - time.time())) |
||||
|
gevent.sleep(delay) |
||||
|
|
||||
|
def run(self): |
||||
|
self.client.set_speaking(True) |
||||
|
|
||||
|
while self.playing: |
||||
|
self.now_playing = self.queue.get() |
||||
|
|
||||
|
self.events.emit(self.Events.START_PLAY, self.now_playing) |
||||
|
self.play_task = gevent.spawn(self.play, self.now_playing) |
||||
|
self.play_task.join() |
||||
|
self.events.emit(self.Events.STOP_PLAY, self.now_playing) |
||||
|
|
||||
|
if self.client.state == VoiceState.DISCONNECTED: |
||||
|
self.playing = False |
||||
|
self.complete.set() |
||||
|
return |
||||
|
|
||||
|
self.client.set_speaking(False) |
||||
|
self.disconnect() |
@ -1,3 +0,0 @@ |
|||||
# Message |
|
||||
|
|
||||
TODO |
|
@ -0,0 +1,52 @@ |
|||||
|
from disco.bot import Plugin |
||||
|
from disco.bot.command import CommandError |
||||
|
from disco.voice.player import Player |
||||
|
from disco.voice.playable import YoutubeDLInput, BufferedOpusEncoderPlayable |
||||
|
from disco.voice.client import VoiceException |
||||
|
|
||||
|
|
||||
|
class MusicPlugin(Plugin): |
||||
|
def load(self, ctx): |
||||
|
super(MusicPlugin, self).load(ctx) |
||||
|
self.guilds = {} |
||||
|
|
||||
|
@Plugin.command('join') |
||||
|
def on_join(self, event): |
||||
|
if event.guild.id in self.guilds: |
||||
|
return event.msg.reply("I'm already playing music here.") |
||||
|
|
||||
|
state = event.guild.get_member(event.author).get_voice_state() |
||||
|
if not state: |
||||
|
return event.msg.reply('You must be connected to voice to use that command.') |
||||
|
|
||||
|
try: |
||||
|
client = state.channel.connect() |
||||
|
except VoiceException as e: |
||||
|
return event.msg.reply('Failed to connect to voice: `{}`'.format(e)) |
||||
|
|
||||
|
self.guilds[event.guild.id] = Player(client) |
||||
|
self.guilds[event.guild.id].complete.wait() |
||||
|
del self.guilds[event.guild.id] |
||||
|
|
||||
|
def get_player(self, guild_id): |
||||
|
if guild_id not in self.guilds: |
||||
|
raise CommandError("I'm not currently playing music here.") |
||||
|
return self.guilds.get(guild_id) |
||||
|
|
||||
|
@Plugin.command('leave') |
||||
|
def on_leave(self, event): |
||||
|
player = self.get_player(event.guild.id) |
||||
|
player.disconnect() |
||||
|
|
||||
|
@Plugin.command('play', '<url:str>') |
||||
|
def on_play(self, event, url): |
||||
|
item = YoutubeDLInput(url).pipe(BufferedOpusEncoderPlayable) |
||||
|
self.get_player(event.guild.id).queue.put(item) |
||||
|
|
||||
|
@Plugin.command('pause') |
||||
|
def on_pause(self, event): |
||||
|
self.get_player(event.guild.id).pause() |
||||
|
|
||||
|
@Plugin.command('resume') |
||||
|
def on_resume(self, event): |
||||
|
self.get_player(event.guild.id).resume() |
@ -0,0 +1,33 @@ |
|||||
|
from disco.bot import Plugin |
||||
|
|
||||
|
|
||||
|
class BasicPlugin(Plugin): |
||||
|
def load(self, ctx): |
||||
|
super(BasicPlugin, self).load(ctx) |
||||
|
self.tags = self.storage.guild('tags') |
||||
|
|
||||
|
@Plugin.command('add', '<name:str> <value:str...>', group='tags') |
||||
|
def on_tags_add(self, event, name, value): |
||||
|
if name in self.tags: |
||||
|
return event.msg.reply('That tag already exists!') |
||||
|
|
||||
|
self.tags[name] = value |
||||
|
return event.msg.reply(u':ok_hand: created the tag {}'.format(name), sanitize=True) |
||||
|
|
||||
|
@Plugin.command('get', '<name:str>', group='tags') |
||||
|
def on_tags_get(self, event, name): |
||||
|
if name not in self.tags: |
||||
|
return event.msg.reply('That tag does not exist!') |
||||
|
|
||||
|
return event.msg.reply(self.tags[name], sanitize=True) |
||||
|
|
||||
|
@Plugin.command('delete', '<name:str>', group='tags', aliases=['del', 'rmv', 'remove']) |
||||
|
def on_tags_delete(self, event, name): |
||||
|
if name not in self.tags: |
||||
|
return event.msg.reply('That tag does not exist!') |
||||
|
|
||||
|
del self.tags[name] |
||||
|
|
||||
|
return event.msg.reply(u':ok_hand: I deleted the {} tag for you'.format( |
||||
|
name |
||||
|
), sanitize=True) |
@ -1,6 +1,6 @@ |
|||||
gevent==1.1.2 |
gevent==1.2.1 |
||||
holster==1.0.11 |
holster==1.0.14 |
||||
inflection==0.3.1 |
inflection==0.3.1 |
||||
requests==2.11.1 |
requests==2.13.0 |
||||
six==1.10.0 |
six==1.10.0 |
||||
websocket-client==0.37.0 |
websocket-client==0.40.0 |
||||
|
@ -0,0 +1,47 @@ |
|||||
|
from unittest import TestCase |
||||
|
|
||||
|
from disco.client import ClientConfig, Client |
||||
|
from disco.bot.bot import Bot |
||||
|
from disco.bot.command import Command |
||||
|
|
||||
|
|
||||
|
class MockBot(Bot): |
||||
|
@property |
||||
|
def commands(self): |
||||
|
return getattr(self, '_commands', []) |
||||
|
|
||||
|
|
||||
|
class TestBot(TestCase): |
||||
|
def setUp(self): |
||||
|
self.client = Client(ClientConfig( |
||||
|
{'config': 'TEST_TOKEN'} |
||||
|
)) |
||||
|
self.bot = MockBot(self.client) |
||||
|
|
||||
|
def test_command_abbreviation(self): |
||||
|
groups = ['config', 'copy', 'copez', 'copypasta'] |
||||
|
result = self.bot.compute_group_abbrev(groups) |
||||
|
self.assertDictEqual(result, { |
||||
|
'config': 'con', |
||||
|
'copypasta': 'copy', |
||||
|
'copez': 'cope', |
||||
|
}) |
||||
|
|
||||
|
def test_command_abbreivation_conflicting(self): |
||||
|
groups = ['cat', 'cap', 'caz', 'cas'] |
||||
|
result = self.bot.compute_group_abbrev(groups) |
||||
|
self.assertDictEqual(result, {}) |
||||
|
|
||||
|
def test_many_commands(self): |
||||
|
self.bot._commands = [ |
||||
|
Command(None, None, 'test{}'.format(i), '<test:str>') |
||||
|
for i in range(1000) |
||||
|
] |
||||
|
|
||||
|
self.bot.compute_command_matches_re() |
||||
|
match = self.bot.command_matches_re.match('test5 123') |
||||
|
self.assertNotEqual(match, None) |
||||
|
|
||||
|
match = self.bot._commands[0].compiled_regex.match('test0 123 456') |
||||
|
self.assertEqual(match.group(1).strip(), 'test0') |
||||
|
self.assertEqual(match.group(2).strip(), '123 456') |
@ -0,0 +1,21 @@ |
|||||
|
from unittest import TestCase |
||||
|
|
||||
|
from disco.types.channel import Channel, ChannelType |
||||
|
|
||||
|
|
||||
|
class TestChannel(TestCase): |
||||
|
def test_nsfw_channel(self): |
||||
|
channel = Channel( |
||||
|
name='nsfw-testing', |
||||
|
type=ChannelType.GUILD_TEXT) |
||||
|
self.assertTrue(channel.is_nsfw) |
||||
|
|
||||
|
channel = Channel( |
||||
|
name='nsfw-testing', |
||||
|
type=ChannelType.GUILD_VOICE) |
||||
|
self.assertFalse(channel.is_nsfw) |
||||
|
|
||||
|
channel = Channel( |
||||
|
name='nsfw_testing', |
||||
|
type=ChannelType.GUILD_TEXT) |
||||
|
self.assertFalse(channel.is_nsfw) |
@ -0,0 +1,32 @@ |
|||||
|
from unittest import TestCase |
||||
|
|
||||
|
from datetime import datetime |
||||
|
from disco.types.message import MessageEmbed |
||||
|
|
||||
|
|
||||
|
class TestEmbeds(TestCase): |
||||
|
def test_empty_embed(self): |
||||
|
embed = MessageEmbed() |
||||
|
self.assertDictEqual( |
||||
|
embed.to_dict(), |
||||
|
{ |
||||
|
'image': {}, |
||||
|
'author': {}, |
||||
|
'video': {}, |
||||
|
'thumbnail': {}, |
||||
|
'footer': {}, |
||||
|
'fields': [], |
||||
|
'type': 'rich', |
||||
|
}) |
||||
|
|
||||
|
def test_embed(self): |
||||
|
embed = MessageEmbed( |
||||
|
title='Test Title', |
||||
|
description='Test Description', |
||||
|
url='https://test.url/' |
||||
|
) |
||||
|
obj = embed.to_dict() |
||||
|
self.assertEqual(obj['title'], 'Test Title') |
||||
|
self.assertEqual(obj['description'], 'Test Description') |
||||
|
self.assertEqual(obj['url'], 'https://test.url/') |
||||
|
|
@ -0,0 +1,42 @@ |
|||||
|
""" |
||||
|
This module tests that all of disco can be imported, mostly to help reduce issues |
||||
|
with untested code that will not even parse/run on Py2/3 |
||||
|
""" |
||||
|
from disco.api.client import * |
||||
|
from disco.api.http import * |
||||
|
from disco.api.ratelimit import * |
||||
|
from disco.bot.bot import * |
||||
|
from disco.bot.command import * |
||||
|
from disco.bot.parser import * |
||||
|
from disco.bot.plugin import * |
||||
|
from disco.bot.storage import * |
||||
|
from disco.gateway.client import * |
||||
|
from disco.gateway.events import * |
||||
|
from disco.gateway.ipc import * |
||||
|
from disco.gateway.packets import * |
||||
|
# Not imported, GIPC is required but not provided by default |
||||
|
# from disco.gateway.sharder import * |
||||
|
from disco.types.base import * |
||||
|
from disco.types.channel import * |
||||
|
from disco.types.guild import * |
||||
|
from disco.types.invite import * |
||||
|
from disco.types.message import * |
||||
|
from disco.types.permissions import * |
||||
|
from disco.types.user import * |
||||
|
from disco.types.voice import * |
||||
|
from disco.types.webhook import * |
||||
|
from disco.util.backdoor import * |
||||
|
from disco.util.config import * |
||||
|
from disco.util.functional import * |
||||
|
from disco.util.hashmap import * |
||||
|
from disco.util.limiter import * |
||||
|
from disco.util.logging import * |
||||
|
from disco.util.serializer import * |
||||
|
from disco.util.snowflake import * |
||||
|
from disco.util.token import * |
||||
|
from disco.util.websocket import * |
||||
|
from disco.voice.client import * |
||||
|
from disco.voice.opus import * |
||||
|
from disco.voice.packets import * |
||||
|
from disco.voice.playable import * |
||||
|
from disco.voice.player import * |
Loading…
Reference in new issue