Browse Source

Merge branch 'master' into feature/audit-log

pull/35/head
Andrei 8 years ago
parent
commit
b8166c2fa6
  1. 2
      disco/__init__.py
  2. 7
      disco/api/client.py
  3. 35
      disco/bot/bot.py
  4. 14
      disco/bot/plugin.py
  5. 10
      disco/cli.py
  6. 5
      disco/gateway/events.py
  7. 4
      disco/state.py
  8. 9
      disco/util/string.py
  9. 1
      docs/installation.md
  10. 83
      examples/basic_plugin.py
  11. 4
      requirements.txt
  12. 2
      setup.py
  13. 23
      tests/test_bot.py

2
disco/__init__.py

@ -1 +1 @@
VERSION = '0.0.10'
VERSION = '0.0.11-rc.1'

7
disco/api/client.py

@ -279,8 +279,11 @@ class APIClient(LoggingClass):
'position': position,
}, headers=_reason_header(reason))
def guilds_members_list(self, guild):
r = self.http(Routes.GUILDS_MEMBERS_LIST, dict(guild=guild))
def guilds_members_list(self, guild, limit=1000, after=None):
r = self.http(Routes.GUILDS_MEMBERS_LIST, dict(guild=guild), params=optional(
limit=limit,
after=after,
))
return GuildMember.create_hash(self.client, 'id', r.json(), guild_id=guild)
def guilds_members_get(self, guild, member):

35
disco/bot/bot.py

@ -1,11 +1,13 @@
import re
import os
import six
import gevent
import inspect
import importlib
from six.moves import reload_module
from holster.threadlocal import ThreadLocal
from gevent.wsgi import WSGIServer
from disco.types.guild import GuildMember
from disco.bot.plugin import Plugin
@ -63,6 +65,13 @@ class BotConfig(Config):
The serialization format plugin configuration files are in.
plugin_config_dir : str
The directory plugin configuration is located within.
http_enabled : bool
Whether to enable the built-in Flask server which allows plugins to handle
and route HTTP requests.
http_host : str
The host string for the HTTP Flask server (if enabled)
http_port : int
The port for the HTTP Flask server (if enabled)
"""
levels = {}
plugins = []
@ -90,6 +99,10 @@ class BotConfig(Config):
storage_serializer = 'json'
storage_path = 'storage.json'
http_enabled = False
http_host = '0.0.0.0'
http_port = 7575
class Bot(LoggingClass):
"""
@ -132,6 +145,13 @@ class Bot(LoggingClass):
if self.client.config.manhole_enable:
self.client.manhole_locals['bot'] = self
if self.config.http_enabled:
from flask import Flask
self.log.info('Starting HTTP server bound to %s:%s', self.config.http_host, self.config.http_port)
self.http = Flask('disco')
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.plugins = {}
self.group_abbrev = {}
@ -275,7 +295,7 @@ class Bot(LoggingClass):
mention_rules.get('role', False) and any(mention_roles),
msg.channel.is_dm
)):
return
return []
if mention_direct:
if msg.guild:
@ -296,17 +316,19 @@ class Bot(LoggingClass):
content = content.lstrip()
if prefix and not content.startswith(prefix):
return
return []
else:
content = content[len(prefix):]
if not self.command_matches_re or not self.command_matches_re.match(content):
return
return []
options = []
for command in self.commands:
match = command.compiled_regex.match(content)
if match:
yield (command, match)
options.append((command, match))
return sorted(options, key=lambda obj: obj[0].group is None)
def get_level(self, actor):
level = CommandLevels.DEFAULT
@ -359,14 +381,13 @@ class Bot(LoggingClass):
if not len(commands):
return False
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 True
return False
def on_message_create(self, event):
if event.message.author.id == self.client.state.me.id:

14
disco/bot/plugin.py

@ -131,6 +131,17 @@ class BasePluginDeco(object):
'kwargs': kwargs,
})
@classmethod
def route(cls, *args, **kwargs):
"""
Adds an HTTP route.
"""
return cls.add_meta_deco({
'type': 'http.add_route',
'args': args,
'kwargs': kwargs,
})
class PluginDeco(BasePluginDeco):
"""
@ -227,6 +238,9 @@ class Plugin(LoggingClass, PluginDeco):
getattr(command.parser, meta['type'].split('.', 1)[-1])(
*meta['args'],
**meta['kwargs'])
elif meta['type'] == 'http.add_route':
meta['kwargs']['view_func'] = member
self.bot.http.add_url_rule(*meta['args'], **meta['kwargs'])
else:
raise Exception('unhandled meta type {}'.format(meta))

10
disco/cli.py

@ -14,7 +14,7 @@ from gevent import monkey
monkey.patch_all()
parser = argparse.ArgumentParser()
parser.add_argument('--config', help='Configuration file', default='config.yaml')
parser.add_argument('--config', help='Configuration file', default='config.json')
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)
@ -25,6 +25,7 @@ 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=[])
parser.add_argument('--log-level', help='log level', default='info')
parser.add_argument('--http-bind', help='bind information for http server', default=None)
def disco_main(run=False):
@ -77,6 +78,13 @@ def disco_main(run=False):
bot_config.plugins = args.plugin
else:
bot_config.plugins += args.plugin
if args.http_bind:
bot_config.http_enabled = True
host, port = args.http_bind.split(':', 1)
bot_config.http_host = host
bot_config.http_port = int(port)
bot = Bot(client, bot_config)
if run:

5
disco/gateway/events.py

@ -1,6 +1,5 @@
from __future__ import print_function
import inflection
import six
from disco.types.user import User, Presence
@ -8,8 +7,8 @@ from disco.types.channel import Channel, PermissionOverwrite
from disco.types.message import Message, MessageReactionEmoji
from disco.types.voice import VoiceState
from disco.types.guild import Guild, GuildMember, Role, GuildEmoji
from disco.types.base import Model, ModelMeta, Field, ListField, AutoDictField, snowflake, datetime
from disco.util.string import underscore
# Mapping of discords event name to our event classes
EVENTS_MAP = {}
@ -20,7 +19,7 @@ class GatewayEventMeta(ModelMeta):
obj = super(GatewayEventMeta, mcs).__new__(mcs, name, parents, dct)
if name != 'GatewayEvent':
EVENTS_MAP[inflection.underscore(name).upper()] = obj
EVENTS_MAP[underscore(name).upper()] = obj
return obj

4
disco/state.py

@ -1,12 +1,12 @@
import six
import weakref
import inflection
from collections import deque, namedtuple
from gevent.event import Event
from disco.types.base import UNSET
from disco.util.config import Config
from disco.util.string import underscore
from disco.util.hashmap import HashMap, DefaultHashMap
@ -131,7 +131,7 @@ class State(object):
assert not len(self.listeners), 'Binding while already bound is dangerous'
for event in self.EVENTS:
func = 'on_' + inflection.underscore(event)
func = 'on_' + underscore(event)
self.listeners.append(self.client.events.on(event, getattr(self, func)))
def fill_messages(self, channel):

9
disco/util/string.py

@ -0,0 +1,9 @@
import re
# Taken from inflection library
def underscore(word):
word = re.sub(r'([A-Z]+)([A-Z][a-z])', r'\1_\2', word)
word = re.sub(r'([a-z\d])([A-Z])', r'\1_\2', word)
word = word.replace('-', '_')
return word.lower()

1
docs/installation.md

@ -21,6 +21,7 @@ pip install disco[performance]
| Name | Explanation | Versions |
|------|-------------|----------|
| voice | Adds functionality required to connect and use voice | Both |
| http | Adds a built-in HTTP server w/ Flask, allowing plugins to handle HTTP requests | Both |
| music | Adds the ability to stream and play music from various third party sites | Both |
| performance | Adds a faster JSON parser (ujson) and an ETF encoding parser | 2.x Only |
| sharding | Adds a library which is required to enable auto-sharding | 2.x Only |

83
examples/basic_plugin.py

@ -1,4 +1,5 @@
from disco.bot import Plugin
from disco.util.sanitize import S
class BasicPlugin(Plugin):
@ -9,76 +10,66 @@ class BasicPlugin(Plugin):
# channel = event.guild.create_channel('audit-log-test', 'text', reason='TEST CREATE')
# channel.delete(reason='TEST AUDIT 2')
@Plugin.command('reload')
def on_reload(self, event):
self.reload()
event.msg.reply('Reloaded!')
@Plugin.command('ban', '<user:snowflake> <reason:str...>')
def on_ban(self, event, user, reason):
event.guild.create_ban(user, reason=reason + u'\U0001F4BF')
@Plugin.command('ping')
def on_ping_command(self, event):
# Generally all the functionality you need to interact with is contained
# within the event object passed to command and event handlers.
event.msg.reply('Pong!')
@Plugin.listen('MessageCreate')
def on_message_create(self, msg):
self.log.info('Message created: {}: {}'.format(msg.author, msg.content))
def on_message_create(self, event):
# All of Discord's events can be listened too and handled easily
self.log.info(u'{}: {}'.format(event.author, event.content))
@Plugin.command('test')
@Plugin.command('echo', '<content:str...>')
def on_echo_command(self, event, content):
event.msg.reply(content)
@Plugin.command('spam', '<count:int> <content:str...>')
def on_spam_command(self, event, count, content):
for i in range(count):
event.msg.reply(content)
@Plugin.command('count', group='messages')
def on_stats(self, event):
msg = event.msg.reply('Ok, one moment...')
msg_count = 0
# Commands can take a set of arguments that are validated by Disco itself
# and content sent via messages can be automatically sanitized to avoid
# mentions/etc.
event.msg.reply(content, santize=True)
for msgs in event.channel.messages_iter(bulk=True):
msg_count += len(msgs)
@Plugin.command('add', '<a:int> <b:int>', group='math')
def on_math_add_command(self, event, a, b):
# Commands can be grouped together for a cleaner user-facing interface.
event.msg.reply('{}'.format(a + b))
msg.edit('{} messages'.format(msg_count))
@Plugin.command('sub', '<a:int> <b:int>', group='math')
def on_math_sub_command(self, event, a, b):
event.msg.reply('{}'.format(a - b))
@Plugin.command('tag', '<name:str> [value:str...]')
def on_tag(self, event, name, value=None):
# Plugins can easily store data locally using Disco's built in storage
tags = self.storage.guild.ensure('tags')
if value:
tags[name] = value
event.msg.reply(':ok_hand:')
event.msg.reply(u':ok_hand: created tag `{}`'.format(S(name)))
else:
if name in tags:
return event.msg.reply(tags[name])
else:
return event.msg.reply('Unknown tag: `{}`'.format(name))
@Plugin.command('info', '<query:str...>')
def on_info(self, event, query):
users = list(self.state.users.select({'username': query}, {'id': query}))
if not users:
event.msg.reply("Couldn't find user for your query: `{}`".format(query))
elif len(users) > 1:
event.msg.reply('I found too many users ({}) for your query: `{}`'.format(len(users), query))
else:
user = users[0]
parts = []
parts.append('ID: {}'.format(user.id))
parts.append('Username: {}'.format(user.username))
parts.append('Discriminator: {}'.format(user.discriminator))
if event.channel.guild:
member = event.channel.guild.get_member(user)
parts.append('Nickname: {}'.format(member.nick))
parts.append('Joined At: {}'.format(member.joined_at))
event.msg.reply('```\n{}\n```'.format(
'\n'.join(parts)
))
return event.msg.reply(u'Unknown tag: `{}`'.format(S(name)))
@Plugin.command('test', parser=True)
@Plugin.parser.add_argument('-a', '--asdf', help='wow')
@Plugin.parser.add_argument('--help', action='store_true')
def on_test(self, event, args):
# Disco supports using an argparse.ArgumentParser for parsing commands as
# well, which helps for large complex commands with many options or flags.
if args.help:
return event.msg.reply(event.parser.format_help())
event.msg.reply(args.asdf)
@Plugin.route('/test')
def on_test_route(self):
# Disco has built-in support for Flask (if installed and enabled) which
# allows plugins to create HTTP routes.
from flask import request
print dict(request.headers)
return 'Hi!'

4
requirements.txt

@ -1,7 +1,5 @@
gevent==1.2.1
gevent==1.2.2
holster==1.0.15
inflection==0.3.1
requests==2.13.0
six==1.10.0
websocket-client==0.40.0
pyyaml==3.12

2
setup.py

@ -18,6 +18,8 @@ with open('README.md') as f:
extras_require = {
'voice': ['pynacl==1.1.2'],
'http': ['flask==0.12.2'],
'yaml': ['pyyaml==3.12'],
'music': ['youtube_dl==2017.4.26'],
'performance': ['erlpack==0.3.2', 'ujson==1.35'],
'sharding': ['gipc==0.6.0'],

23
tests/test_bot.py

@ -69,3 +69,26 @@ class TestBot(TestCase):
self.assertNotEqual(self.bot.command_matches_re.match('t b'), None)
self.assertEqual(self.bot.command_matches_re.match('testing b'), None)
self.assertEqual(self.bot.command_matches_re.match('testlmao a'), None)
def test_group_and_command(self):
plugin = Object()
plugin.bot = self.bot
self.bot._commands = [
Command(plugin, None, 'test'),
Command(plugin, None, 'a', group='test'),
Command(plugin, None, 'b', group='test'),
]
self.bot.recompute()
msg = Object()
msg.content = '!test a'
commands = list(self.bot.get_commands_for_message(False, None, '!', msg))
self.assertEqual(commands[0][0], self.bot._commands[1])
self.assertEqual(commands[1][0], self.bot._commands[0])
msg.content = '!test'
commands = list(self.bot.get_commands_for_message(False, None, '!', msg))
self.assertEqual(commands[0][0], self.bot._commands[0])

Loading…
Cancel
Save