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

Commit

Permalink
no seriously make small commits
Browse files Browse the repository at this point in the history
- Storage backends take a config
- Add command permissions
- Add ability to listen to BOTH incoming and outgoing gateway packets
- Heavily refactor cli, now prefer loading from config with options as
overrides
- Add debug function to gateway events, helps with figuring data out w/o
spamming console
- Change Channel.last_message_id from property that looks in the message
tracking deque, to a attribute that gets updated by the state module
- Add State.fill_messages for backfilling the messages store
- Handle MessageDeleteBulk in State
- Add some helper functions for hash/equality model functions
- Fix MessageIterator
- Add Channel.delete_message, Channel.delete_messages
- Some more functional stuff
- Snowflake timestamp conversion
- Bump holster
  • Loading branch information
b1naryth1ef committed Oct 9, 2016
1 parent feb90bd commit 6036dd8
Show file tree
Hide file tree
Showing 18 changed files with 318 additions and 114 deletions.
3 changes: 2 additions & 1 deletion disco/bot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from disco.bot.bot import Bot, BotConfig
from disco.bot.plugin import Plugin
from disco.bot.command import CommandLevels
from disco.util.config import Config

__all__ = ['Bot', 'BotConfig', 'Plugin', 'Config']
__all__ = ['Bot', 'BotConfig', 'Plugin', 'Config', 'CommandLevels']
13 changes: 1 addition & 12 deletions disco/bot/backends/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,6 @@


class MemoryBackend(BaseStorageBackend):
def __init__(self):
def __init__(self, config):
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]
66 changes: 56 additions & 10 deletions disco/bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
from six.moves import reload_module
from holster.threadlocal import ThreadLocal

from disco.types.guild import GuildMember
from disco.bot.plugin import Plugin
from disco.bot.command import CommandEvent
from disco.bot.command import CommandEvent, CommandLevels
from disco.bot.storage import Storage
from disco.util.config import Config
from disco.util.serializer import Serializer
Expand All @@ -20,9 +21,11 @@ class BotConfig(Config):
Attributes
----------
token : str
The authentication token for this bot. This is passed on to the
:class:`disco.client.Client` without any validation.
levels : dict(snowflake, str)
Mapping of user IDs/role IDs to :class:`disco.bot.commands.CommandLevesls`
which is used for the default commands_level_getter.
plugins : list[string]
List of plugin modules to load.
commands_enabled : bool
Whether this bot instance should utilize command parsing. Generally this
should be true, unless your bot is only handling events and has no user
Expand All @@ -42,17 +45,21 @@ class BotConfig(Config):
If true, the bot will reparse an edited message if it was the last sent
message in a channel, and did not previously trigger a command. This is
helpful for allowing edits to typod commands.
commands_level_getter : function
If set, a function which when given a GuildMember or User, returns the
relevant :class:`disco.bot.commands.CommandLevels`.
plugin_config_provider : Optional[function]
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.
The serialization format plugin configuration files are in.
plugin_config_dir : str
The directory plugin configuration is located within.
"""
token = None
levels = {}
plugins = {}

commands_enabled = True
commands_require_mention = True
Expand All @@ -64,6 +71,7 @@ class BotConfig(Config):
}
commands_prefix = ''
commands_allow_edit = True
commands_level_getter = None

plugin_config_provider = None
plugin_config_format = 'yaml'
Expand Down Expand Up @@ -127,6 +135,14 @@ def __init__(self, client, config=None):
# Stores a giant regex matcher for all commands
self.command_matches_re = None

# Finally, load all the plugin modules that where passed with the config
for plugin_mod in self.config.plugins:
self.add_plugin_module(plugin_mod)

# Convert level mapping
for k, v in self.config.levels.items():
self.config.levels[k] = CommandLevels.get(v)

@classmethod
def from_cli(cls, *plugins):
"""
Expand Down Expand Up @@ -225,6 +241,32 @@ def get_commands_for_message(self, msg):
if match:
yield (command, match)

def get_level(self, actor):
level = CommandLevels.DEFAULT

if callable(self.config.commands_level_getter):
level = self.config.commands_level_getter(actor)
else:
if actor.id in self.config.levels:
level = self.config.levels[actor.id]

if isinstance(actor, GuildMember):
for rid in actor.roles:
if rid in self.config.levels and self.config.levels[rid] > level:
level = self.config.levels[rid]

return level

def check_command_permissions(self, command, msg):
if not command.level:
return True

level = self.get_level(msg.author if not msg.guild else msg.guild.get_member(msg.author))

if level >= command.level:
return True
return False

def handle_message(self, msg):
"""
Attempts to handle a newly created or edited message in the context of
Expand All @@ -243,10 +285,14 @@ def handle_message(self, msg):
commands = list(self.get_commands_for_message(msg))

if len(commands):
return any([
command.plugin.execute(CommandEvent(command, msg, match))
for command, match in commands
])
result = False
for command, match in commands:
if not self.check_command_permissions(command, msg):
continue

if command.plugin.execute(CommandEvent(command, msg, match)):
result = True
return result

return False

Expand Down
17 changes: 15 additions & 2 deletions disco/bot/command.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import re

from holster.enum import Enum

from disco.bot.parser import ArgumentSet, ArgumentError
from disco.util.functional import cached_property

REGEX_FMT = '({})'
ARGS_REGEX = '( (.*)$|$)'
MENTION_RE = re.compile('<@!?([0-9]+)>')

CommandLevels = Enum(
DEFAULT=0,
TRUSTED=10,
MOD=50,
ADMIN=100,
OWNER=500,
)


class CommandEvent(object):
"""
Expand All @@ -33,7 +43,7 @@ def __init__(self, command, msg, match):
self.msg = msg
self.match = match
self.name = self.match.group(1)
self.args = self.match.group(2).strip().split(' ')
self.args = [i for i in self.match.group(2).strip().split(' ') if i]

@cached_property
def member(self):
Expand Down Expand Up @@ -93,7 +103,9 @@ class Command(object):
is_regex : Optional[bool]
Whether the triggers for this command should be treated as raw regex.
"""
def __init__(self, plugin, func, trigger, args=None, aliases=None, group=None, is_regex=False):
def __init__(self, plugin, func, trigger, args=None, level=None,
aliases=None, group=None, is_regex=False):

self.plugin = plugin
self.func = func
self.triggers = [trigger] + (aliases or [])
Expand All @@ -110,6 +122,7 @@ def resolve_user(ctx, id):
'role': self.mention_type([resolve_role], force=True),
})

self.level = level
self.group = group
self.is_regex = is_regex

Expand Down
37 changes: 31 additions & 6 deletions disco/bot/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,23 @@ def listen(cls, event_name, priority=None):
"""
return cls.add_meta_deco({
'type': 'listener',
'event_name': event_name,
'what': 'event',
'desc': event_name,
'priority': priority
})

@classmethod
def listen_packet(cls, op, priority=None):
"""
Binds the function to listen for a given gateway op code
"""
return cls.add_meta_deco({
'type': 'listener',
'what': 'packet',
'desc': op,
'priority': priority,
})

@classmethod
def command(cls, *args, **kwargs):
"""
Expand Down Expand Up @@ -155,7 +168,7 @@ def bind_all(self):
if hasattr(member, 'meta'):
for meta in member.meta:
if meta['type'] == 'listener':
self.register_listener(member, meta['event_name'], meta['priority'])
self.register_listener(member, meta['what'], meta['desc'], meta['priority'])
elif meta['type'] == 'command':
self.register_command(member, *meta['args'], **meta['kwargs'])
elif meta['type'] == 'schedule':
Expand Down Expand Up @@ -205,21 +218,33 @@ def _dispatch(self, typ, func, event, *args, **kwargs):

return True

def register_listener(self, func, name, priority):
def register_listener(self, func, what, desc, priority):
"""
Registers a listener
Parameters
----------
what : str
What the listener is for (event, packet)
func : function
The function to be registered.
name : string
Name of event to listen for.
desc
The descriptor of the event/packet.
priority : Priority
The priority of this listener.
"""
func = functools.partial(self._dispatch, 'listener', func)
self.listeners.append(self.bot.client.events.on(name, func, priority=priority or Priority.NONE))

priority = priority or Priority.NONE

if what == 'event':
li = self.bot.client.events.on(desc, func, priority=priority)
elif what == 'packet':
li = self.bot.client.packets.on(desc, func, priority=priority)
else:
raise Exception('Invalid listener what: {}'.format(what))

self.listeners.append(li)

def register_command(self, func, *args, **kwargs):
"""
Expand Down
54 changes: 28 additions & 26 deletions disco/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""
from __future__ import print_function

import os
import logging
import argparse

Expand All @@ -12,13 +13,14 @@
monkey.patch_all()

parser = argparse.ArgumentParser()
parser.add_argument('--token', help='Bot Authentication Token', required=True)
parser.add_argument('--shard-count', help='Total number of shards', default=1)
parser.add_argument('--shard-id', help='Current shard number/id', default=0)
parser.add_argument('--manhole', action='store_true', help='Enable the manhole', default=False)
parser.add_argument('--manhole-bind', help='host:port for the manhole to bind too', default='localhost:8484')
parser.add_argument('--encoder', help='encoder for gateway data', default='json')
parser.add_argument('--bot', help='run a disco bot on this client', action='store_true', default=False)
parser.add_argument('--config', help='Configuration file', default='config.yaml')
parser.add_argument('--token', help='Bot Authentication Token', default=None)
parser.add_argument('--shard-count', help='Total number of shards', default=None)
parser.add_argument('--shard-id', help='Current shard number/id', default=None)
parser.add_argument('--manhole', action='store_true', help='Enable the manhole', default=None)
parser.add_argument('--manhole-bind', help='host:port for the manhole to bind too', default=None)
parser.add_argument('--encoder', help='encoder for gateway data', default=None)
parser.add_argument('--run-bot', help='run a disco bot on this client', action='store_true', default=False)
parser.add_argument('--plugin', help='load plugins into the bot', nargs='*', default=[])

logging.basicConfig(level=logging.INFO)
Expand All @@ -37,34 +39,34 @@ def disco_main(run=False):
args = parser.parse_args()

from disco.client import Client, ClientConfig
from disco.bot import Bot
from disco.gateway.encoding import ENCODERS
from disco.bot import Bot, BotConfig
from disco.util.token import is_valid_token

if not is_valid_token(args.token):
print('Invalid token passed')
return
if os.path.exists(args.config):
config = ClientConfig.from_file(args.config)
else:
config = ClientConfig()

cfg = ClientConfig()
cfg.token = args.token
cfg.shard_id = args.shard_id
cfg.shard_count = args.shard_count
cfg.manhole_enable = args.manhole
cfg.manhole_bind = args.manhole_bind
cfg.encoding_cls = ENCODERS[args.encoder]
for k, v in vars(args).items():
if hasattr(config, k) and v is not None:
setattr(config, k, v)

client = Client(cfg)
if not is_valid_token(config.token):
print('Invalid token passed')
return

if args.bot:
bot = Bot(client)
client = Client(config)

for plugin in args.plugin:
bot.add_plugin_module(plugin)
bot = None
if args.run_bot or hasattr(config, 'bot'):
bot_config = BotConfig(config.bot) if hasattr(config, 'bot') else BotConfig()
bot_config.plugins += args.plugin
bot = Bot(client, bot_config)

if run:
client.run_forever()
(bot or client).run_forever()

return client
return (bot or client)

if __name__ == '__main__':
disco_main(True)

0 comments on commit 6036dd8

Please sign in to comment.