Skip to content
This repository has been archived by the owner on Aug 1, 2021. It is now read-only.

Commit

Permalink
todo: make small commits
Browse files Browse the repository at this point in the history
- Add the concept of storage backends, not fully fleshed out at this
point, but a good starting point
- Add a generic serializer
- Move mention_nick to the GuildMember object (I'm not sure this was a
good idea, but we'll see)
- Add a default config loader to the bot
- Fix some Python 2.x/3.x unicode stuff
- Start tracking greenlets on the Plugin level, this will help with
reloading when its fully completed
- Fix manhole locals being basically empty (sans the bot if relevant)
- Add Channel.delete_messages_bulk
- Add GuildMember.owner to check if the member owns the server
  • Loading branch information
b1naryth1ef committed Oct 9, 2016
1 parent e3140b6 commit 7d53702
Show file tree
Hide file tree
Showing 17 changed files with 310 additions and 54 deletions.
3 changes: 2 additions & 1 deletion disco/bot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from disco.bot.bot import Bot, BotConfig
from disco.bot.plugin import Plugin
from disco.util.config import Config

__all__ = ['Bot', 'BotConfig', 'Plugin']
__all__ = ['Bot', 'BotConfig', 'Plugin', 'Config']
8 changes: 8 additions & 0 deletions disco/bot/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from .memory import MemoryBackend
from .disk import DiskBackend


BACKENDS = {
'memory': MemoryBackend,
'disk': DiskBackend,
}
20 changes: 20 additions & 0 deletions disco/bot/backends/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

class BaseStorageBackend(object):
def base(self):
return self.storage

def __getitem__(self, key):
return self.storage[key]

def __setitem__(self, key, value):
self.storage[key] = value

def __delitem__(self, key):
del self.storage[key]


class StorageDict(dict):
def ensure(self, name):
if not dict.__contains__(self, name):
dict.__setitem__(self, name, StorageDict())
return dict.__getitem__(self, name)
35 changes: 35 additions & 0 deletions disco/bot/backends/disk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import os

from .base import BaseStorageBackend, StorageDict


class DiskBackend(BaseStorageBackend):
def __init__(self, config):
self.format = config.get('format', 'json')
self.path = config.get('path', 'storage') + '.' + self.format
self.storage = StorageDict()

@staticmethod
def get_format_functions(fmt):
if fmt == 'json':
from json import loads, dumps
return (loads, dumps)
elif fmt == 'yaml':
from pyyaml import load, dump
return (load, dump)
raise Exception('Unsupported format type {}'.format(fmt))

def load(self):
if not os.path.exists(self.path):
return

decode, _ = self.get_format_functions(self.format)

with open(self.path, 'r') as f:
self.storage = decode(f.read())

def dump(self):
_, encode = self.get_format_functions(self.format)

with open(self.path, 'w') as f:
f.write(encode(self.storage))
18 changes: 18 additions & 0 deletions disco/bot/backends/memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from .base import BaseStorageBackend, StorageDict


class MemoryBackend(BaseStorageBackend):
def __init__(self):
self.storage = StorageDict()

def base(self):
return self.storage

def __getitem__(self, key):
return self.storage[key]

def __setitem__(self, key, value):
self.storage[key] = value

def __delitem__(self, key):
del self.storage[key]
67 changes: 57 additions & 10 deletions disco/bot/bot.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
import os
import importlib
import inspect

Expand All @@ -7,10 +8,12 @@

from disco.bot.plugin import Plugin
from disco.bot.command import CommandEvent
# from disco.bot.storage import Storage
from disco.bot.storage import Storage
from disco.util.config import Config
from disco.util.serializer import Serializer


class BotConfig(object):
class BotConfig(Config):
"""
An object which is used to configure and define the runtime configuration for
a bot.
Expand Down Expand Up @@ -40,9 +43,14 @@ class BotConfig(object):
message in a channel, and did not previously trigger a command. This is
helpful for allowing edits to typod commands.
plugin_config_provider : Optional[function]
If set, this function will be called before loading a plugin, with the
plugins class. Its expected to return a type of configuration object the
plugin understands.
If set, this function will replace the default configuration loading
function, which normally attempts to load a file located at config/plugin_name.fmt
where fmt is the plugin_config_format. The function here should return
a valid configuration object which the plugin understands.
plugin_config_format : str
The serilization format plugin configuration files are in.
plugin_config_dir : str
The directory plugin configuration is located within.
"""
token = None

Expand All @@ -58,6 +66,13 @@ class BotConfig(object):
commands_allow_edit = True

plugin_config_provider = None
plugin_config_format = 'yaml'
plugin_config_dir = 'config'

storage_enabled = False
storage_backend = 'memory'
storage_autosave = True
storage_autosave_interval = 120


class Bot(object):
Expand Down Expand Up @@ -90,7 +105,9 @@ def __init__(self, client, config=None):
self.ctx = ThreadLocal()

# The storage object acts as a dynamic contextual aware store
# self.storage = Storage(self.ctx)
self.storage = None
if self.config.storage_enabled:
self.storage = Storage(self.ctx, self.config.from_prefix('storage'))

if self.client.config.manhole_enable:
self.client.manhole_locals['bot'] = self
Expand Down Expand Up @@ -181,8 +198,12 @@ def get_commands_for_message(self, msg):
raise StopIteration

if mention_direct:
content = content.replace(self.client.state.me.mention, '', 1)
content = content.replace(self.client.state.me.mention_nick, '', 1)
if msg.guild:
member = msg.guild.get_member(self.client.state.me)
if member:
content = content.replace(member.mention, '', 1)
else:
content = content.replace(self.client.state.me.mention, '', 1)
elif mention_everyone:
content = content.replace('@everyone', '', 1)
else:
Expand Down Expand Up @@ -265,8 +286,11 @@ def add_plugin(self, cls, config=None):
if cls.__name__ in self.plugins:
raise Exception('Cannot add already added plugin: {}'.format(cls.__name__))

if not config and callable(self.config.plugin_config_provider):
config = self.config.plugin_config_provider(cls)
if not config:
if callable(self.config.plugin_config_provider):
config = self.config.plugin_config_provider(cls)
else:
config = self.load_plugin_config(cls)

self.plugins[cls.__name__] = cls(self, config)
self.plugins[cls.__name__].load()
Expand Down Expand Up @@ -317,3 +341,26 @@ def add_plugin_module(self, path, config=None):
break
else:
raise Exception('Could not find any plugins to load within module {}'.format(path))

def load_plugin_config(self, cls):
name = cls.__name__.lower()
if name.startswith('plugin'):
name = name[6:]

path = os.path.join(
self.config.plugin_config_dir, name) + '.' + self.config.plugin_config_format

if not os.path.exists(path):
if hasattr(cls, 'config_cls'):
return cls.config_cls()
return

with open(path, 'r') as f:
data = Serializer.loads(self.config.plugin_config_format, f.read())

if hasattr(cls, 'config_cls'):
inst = cls.config_cls()
inst.update(data)
return inst

return data
5 changes: 3 additions & 2 deletions disco/bot/parser.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
import six
import copy


Expand All @@ -7,7 +8,7 @@

# Mapping of types
TYPE_MAP = {
'str': lambda ctx, data: str(data),
'str': lambda ctx, data: str(data) if six.PY3 else unicode(data),
'int': lambda ctx, data: int(data),
'float': lambda ctx, data: int(data),
'snowflake': lambda ctx, data: int(data),
Expand Down Expand Up @@ -160,7 +161,7 @@ def parse(self, rawargs, ctx=None):
try:
raw[idx] = self.convert(ctx, arg.types, r)
except:
raise ArgumentError('cannot convert `{}` to `{}`'.format(
raise ArgumentError(u'cannot convert `{}` to `{}`'.format(
r, ', '.join(arg.types)
))

Expand Down
64 changes: 36 additions & 28 deletions disco/bot/plugin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import inspect
import functools
import gevent
import os
import weakref

from holster.emitter import Priority

Expand All @@ -27,6 +27,16 @@ def deco(f):
return f
return deco

@classmethod
def with_config(cls, config_cls):
"""
Sets the plugins config class to the specified config class.
"""
def deco(plugin_cls):
plugin_cls.config_cls = config_cls
return plugin_cls
return deco

@classmethod
def listen(cls, event_name, priority=None):
"""
Expand Down Expand Up @@ -86,13 +96,14 @@ def post_listener(cls):
})

@classmethod
def schedule(cls, interval=60):
def schedule(cls, *args, **kwargs):
"""
Runs a function repeatedly, waiting for a specified interval
"""
return cls.add_meta_deco({
'type': 'schedule',
'interval': interval,
'args': args,
'kwargs': kwargs,
})


Expand Down Expand Up @@ -131,10 +142,15 @@ def bind_all(self):
self.listeners = []
self.commands = {}
self.schedules = {}
self.greenlets = weakref.WeakSet()

self._pre = {'command': [], 'listener': []}
self._post = {'command': [], 'listener': []}

# TODO: when handling events/commands we need to track the greenlet in
# the greenlets set so we can termiante long running commands/listeners
# on reload.

for name, member in inspect.getmembers(self, predicate=inspect.ismethod):
if hasattr(member, 'meta'):
for meta in member.meta:
Expand All @@ -143,11 +159,16 @@ def bind_all(self):
elif meta['type'] == 'command':
self.register_command(member, *meta['args'], **meta['kwargs'])
elif meta['type'] == 'schedule':
self.register_schedule(member, meta['interval'])
self.register_schedule(member, *meta['args'], **meta['kwargs'])
elif meta['type'].startswith('pre_') or meta['type'].startswith('post_'):
when, typ = meta['type'].split('_', 1)
self.register_trigger(typ, when, member)

def spawn(self, method, *args, **kwargs):
obj = gevent.spawn(method, *args, **kwargs)
self.greenlets.add(obj)
return obj

def execute(self, event):
"""
Executes a CommandEvent this plugin owns
Expand Down Expand Up @@ -217,7 +238,7 @@ def register_command(self, func, *args, **kwargs):
wrapped = functools.partial(self._dispatch, 'command', func)
self.commands[func.__name__] = Command(self, wrapped, *args, **kwargs)

def register_schedule(self, func, interval):
def register_schedule(self, func, interval, repeat=True, init=True):
"""
Registers a function to be called repeatedly, waiting for an interval
duration.
Expand All @@ -230,11 +251,16 @@ def register_schedule(self, func, interval):
Interval (in seconds) to repeat the function on.
"""
def repeat():
while True:
if init:
func()

while True:
gevent.sleep(interval)
func()
if not repeat:
break

self.schedules[func.__name__] = gevent.spawn(repeat)
self.schedules[func.__name__] = self.spawn(repeat)

def load(self):
"""
Expand All @@ -246,6 +272,9 @@ def unload(self):
"""
Called when the plugin is unloaded
"""
for greenlet in self.greenlets:
greenlet.kill()

for listener in self.listeners:
listener.remove()

Expand All @@ -254,24 +283,3 @@ def unload(self):

def reload(self):
self.bot.reload_plugin(self.__class__)

@staticmethod
def load_config_from_path(cls, path, format='json'):
inst = cls()

if not os.path.exists(path):
return inst

with open(path, 'r') as f:
data = f.read()

if format == 'json':
import json
inst.__dict__.update(json.loads(data))
elif format == 'yaml':
import yaml
inst.__dict__.update(yaml.load(data))
else:
raise Exception('Unsupported config format {}'.format(format))

return inst
21 changes: 21 additions & 0 deletions disco/bot/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from .backends import BACKENDS


class Storage(object):
def __init__(self, ctx, config):
self.ctx = ctx
self.backend = BACKENDS[config.backend]
# TODO: autosave
# config.autosave config.autosave_interval

@property
def guild(self):
return self.backend.base().ensure('guilds').ensure(self.ctx['guild'].id)

@property
def channel(self):
return self.backend.base().ensure('channels').ensure(self.ctx['channel'].id)

@property
def user(self):
return self.backend.base().ensure('users').ensure(self.ctx['user'].id)

0 comments on commit 7d53702

Please sign in to comment.