diff --git a/README.md b/README.md index af65002..7fffd0f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,54 @@ # disco -A Pythonic Discord library built for simplicity and extendability. Supports 2.x/3.x versions of Python. Uses gevent. +Disco is a simple and extendable library for the [Discord API](https://discordapp.com/developers/docs/intro). +- Expressive, functional interface that gets out of the way +- Built for high-performance and efficiency +- Configurable and modular, take the bits you need +- Full support for Python 2.x/3.x +- Evented networking and IO using Gevent -## TODOS -- Permissions/Roles support -- Plugin/Library reloading - - Abstract GatewayClient out into the WebsocketProcessProxy - - Abstract VoiceClients to run in a seperate process -- Voice Support +## Installation +Disco was built to run both as a generic-use library, and a standalone bot toolkit. Installing disco is as easy as running `pip install disco`, however some extra packages are recommended for power-users, namely: + +|Name|Reason| +|requests[security]|adds packages for a proper SSL implementation| +|rapidjson|provides a Python implementation of the C rapidjson library, improves performance| + +## Examples + +Simple bot using the builtin bot authoring tools: + +```python +from disco.bot import Bot, Plugin + + +class SimplePlugin(Plugin): + # Plugins provide an easy interface for listening to Discord events + @Plugin.listen('ChannelCreate') + def on_channel_create(self, event): + event.channel.send_message('Woah, a new channel huh!') + + # They also provide an easy-to-use command component + @Plugin.command('ping') + def on_ping_command(self, event): + event.msg.reply('Pong!') + + # Which includes command argument parsing + @Plugin.command('echo', '') + def on_echo_command(self, event, content): + event.msg.reply(content) + +if __name__ == '__main__': + Bot.from_cli( + SimplePlugin + ).run_forever() +``` + +Using the default bot configuration, we can now run this script like so: + +`./simple.py --token="MY_DISCORD_TOKEN"` + +And commands can be triggered by mentioning the bot (configued by the BotConfig.command\_require\_mention flag): + +![](http://i.imgur.com/Vw6T8bi.png) diff --git a/disco/bot/__init__.py b/disco/bot/__init__.py index e8337f1..027ba35 100644 --- a/disco/bot/__init__.py +++ b/disco/bot/__init__.py @@ -1 +1,4 @@ from disco.bot.bot import Bot +from disco.bot.plugin import Plugin + +__all__ = ['Bot', 'Plugin'] diff --git a/disco/bot/bot.py b/disco/bot/bot.py index c047e7d..356aab9 100644 --- a/disco/bot/bot.py +++ b/disco/bot/bot.py @@ -52,6 +52,16 @@ class Bot(object): # Stores a giant regex matcher for all commands self.command_matches_re = None + @classmethod + def from_cli(cls, *plugins): + from disco.cli import disco_main + inst = cls(disco_main()) + + for plugin in plugins: + inst.add_plugin(plugin) + + return inst + @property def commands(self): for plugin in self.plugins.values(): @@ -113,14 +123,16 @@ class Bot(object): def on_message_update(self, event): if self.config.command_allow_edit: - msg = self.last_message_cache.get(event.message.channel_id) - if msg and event.message.id == msg[0].id: - triggered = msg[1] + obj = self.last_message_cache.get(event.message.channel_id) + if not obj: + return - if not triggered: - triggered = self.handle_message(event.message) + msg, triggered = obj + if msg.id == event.message.id and not triggered: + msg.update(event.message) + triggered = self.handle_message(msg) - self.last_message_cache[event.message.channel_id] = (event.message, triggered) + self.last_message_cache[msg.channel_id] = (msg, triggered) def add_plugin(self, cls): if cls.__name__ in self.plugins: diff --git a/disco/cli.py b/disco/cli.py index 17975f2..5bced0e 100644 --- a/disco/cli.py +++ b/disco/cli.py @@ -8,6 +8,8 @@ from gevent import monkey 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) logging.basicConfig(level=logging.INFO) @@ -22,7 +24,7 @@ def disco_main(): return from disco.client import DiscoClient - return DiscoClient(args.token) + return DiscoClient.from_cli(args) if __name__ == '__main__': disco_main().run_forever() diff --git a/disco/client.py b/disco/client.py index e7872c8..5895360 100644 --- a/disco/client.py +++ b/disco/client.py @@ -23,6 +23,18 @@ class DiscoClient(object): self.api = APIClient(self) self.gw = GatewayClient(self) + @classmethod + def from_cli(cls, args): + inst = cls(args.token) + inst.set_shard(args.shard_id, args.shard_count) + return inst + + def set_shard(self, shard_number, shard_count): + self.sharding = { + 'number': shard_number, + 'total': shard_count, + } + def run(self): return gevent.spawn(self.gw.run) diff --git a/disco/state.py b/disco/state.py index 71b8ae4..b2463a4 100644 --- a/disco/state.py +++ b/disco/state.py @@ -19,7 +19,7 @@ class StateConfig(object): class State(object): EVENTS = [ 'Ready', 'GuildCreate', 'GuildUpdate', 'GuildDelete', 'GuildMemberAdd', 'GuildMemberRemove', - 'GuildMemberUpdate', 'GuildMemberChunk', 'GuildRoleCreate', 'GuildRoleUpdate', 'GuildRoleDelete', + 'GuildMemberUpdate', 'GuildMembersChunk', 'GuildRoleCreate', 'GuildRoleUpdate', 'GuildRoleDelete', 'ChannelCreate', 'ChannelUpdate', 'ChannelDelete', 'VoiceStateUpdate' ] @@ -161,7 +161,7 @@ class State(object): del self.guilds[event.guild_id].members[event.user.id] - def on_guild_member_chunk(self, event): + def on_guild_members_chunk(self, event): if event.guild_id not in self.guilds: return diff --git a/examples/basic_plugin.py b/examples/basic_plugin.py index 11a1b66..3bc1746 100644 --- a/examples/basic_plugin.py +++ b/examples/basic_plugin.py @@ -4,8 +4,7 @@ import json from disco import VERSION from disco.cli import disco_main -from disco.bot import Bot -from disco.bot.plugin import Plugin +from disco.bot import Bot, Plugin from disco.types.permissions import Permissions diff --git a/requirements.txt b/requirements.txt index 80711cd..fae8b50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,8 @@ -backports.ssl-match-hostname==3.5.0.1 -cffi==1.8.3 -click==6.6 -cryptography==1.5 -enum34==1.1.6 -Flask==0.11.1 gevent==1.1.2 gipc==0.6.0 -greenlet==0.4.10 -holster==1.0.0 -idna==2.1 +holster==1.0.1 inflection==0.3.1 -ipaddress==1.0.17 -itsdangerous==0.24 -Jinja2==2.8 -MarkupSafe==0.23 -ndg-httpsclient==0.4.2 -pyasn1==0.1.9 -pycparser==2.14 -pyOpenSSL==16.1.0 requests==2.11.1 six==1.10.0 # skema==0.0.1 websocket-client==0.37.0 -Werkzeug==0.11.11 -wheel==0.24.0