Browse Source

documentation 2.0

pull/3/head
Andrei 9 years ago
parent
commit
94de67c6ae
  1. 29
      disco/api/http.py
  2. 68
      disco/api/ratelimit.py
  3. 152
      disco/bot/bot.py
  4. 47
      disco/bot/command.py
  5. 32
      disco/bot/parser.py
  6. 44
      disco/bot/plugin.py
  7. 13
      disco/cli.py
  8. 54
      disco/client.py
  9. 4
      disco/gateway/client.py
  10. 83
      disco/state.py
  11. 128
      disco/types/channel.py
  12. 145
      disco/types/guild.py
  13. 24
      disco/types/invite.py
  14. 139
      disco/types/message.py
  15. 2
      disco/voice/client.py
  16. 150
      docs/api.rst
  17. 1
      docs/conf.py
  18. 38
      docs/disco.api.rst
  19. 46
      docs/disco.bot.rst
  20. 38
      docs/disco.gateway.rst
  21. 50
      docs/disco.rst
  22. 78
      docs/disco.types.rst
  23. 62
      docs/disco.util.rst
  24. 30
      docs/disco.voice.rst
  25. 19
      docs/index.rst
  26. 7
      docs/modules.rst
  27. 7
      docs/setup.rst
  28. 5
      docs/tutorial.rst

29
disco/api/http.py

@ -124,14 +124,37 @@ class HTTPClient(LoggingClass):
}
def __call__(self, route, args=None, **kwargs):
return self.call(route, args, **kwargs)
def call(self, route, args=None, **kwargs):
"""
Makes a request to the given route (as specified in
:class:`disco.api.http.Routes`) with a set of URL arguments, and keyword
arguments passed to requests.
:param route: the method/url route combination to call
:param args: url major arguments (used for Discord rate limits)
:param kwargs: any keyword arguments to be passed along to requests
Parameters
----------
route : tuple(:class:`HTTPMethod`, str)
The method.URL combination that when compiled with URL arguments
creates a requestable route which the HTTPClient will make the
request too.
args : dict(str, str)
A dictionary of URL arguments that will be compiled with the raw URL
to create the requestable route. The HTTPClient uses this to track
rate limits as well.
kwargs : dict
Keyword arguments that will be passed along to the requests library
Raises
------
APIException
Raised when an unrecoverable error occurs, or when we've exhausted
the number of retries.
Returns
-------
:class:`requests.Response`
The response object for the request
"""
args = args or {}
retry = kwargs.pop('retry_number', 0)

68
disco/api/ratelimit.py

@ -7,10 +7,26 @@ class RouteState(object):
An object which stores ratelimit state for a given method/url route
combination (as specified in :class:`disco.api.http.Routes`).
:ivar route: the route this state pertains too
:ivar remaining: the number of requests remaining before the rate limit is hit
:ivar reset_time: unix timestamp (in seconds) when this rate limit is reset
:ivar event: a :class:`gevent.event.Event` used for ratelimit cooldowns
Parameters
----------
route : tuple(HTTPMethod, str)
The route which this RouteState is for.
response : :class:`requests.Response`
The response object for the last request made to the route, should contain
the standard rate limit headers.
Attributes
---------
route : tuple(HTTPMethod, str)
The route which this RouteState is for.
remaining : int
The number of remaining requests to the route before the rate limit will
be hit, triggering a 429 response.
reset_time : int
A unix epoch timestamp (in seconds) after which this rate limit is reset
event : :class:`gevent.event.Event`
An event that is used to block all requests while a route is in the
cooldown stage.
"""
def __init__(self, route, response):
self.route = route
@ -55,9 +71,18 @@ class RouteState(object):
"""
Waits until this route is no longer under a cooldown
:param timeout: timeout after which waiting will be given up
Parameters
----------
timeout : Optional[int]
A timeout (in seconds) after which we will give up waiting
Returns
-------
bool
False if the timeout period expired before the cooldown was finished
"""
self.event.wait(timeout)
return self.event.wait(timeout)
def cooldown(self):
"""
@ -76,7 +101,11 @@ class RateLimiter(object):
"""
A in-memory store of ratelimit states for all routes we've ever called.
:ivar states: a Route -> RouteState mapping
Attributes
----------
states : dict(tuple(HTTPMethod, str), :class:`RouteState`)
Contains a :class:`RouteState` for each route the RateLimiter is currently
tracking.
"""
def __init__(self):
self.states = {}
@ -89,9 +118,19 @@ class RateLimiter(object):
the route is finished being cooled down. This function should be called
before making a request to the specified route.
:param route: route to be checked
:param timeout: an optional timeout after which we'll stop waiting for
the cooldown to complete.
Parameters
----------
route : tuple(HTTPMethod, str)
The route that will be checked.
timeout : Optional[int]
A timeout after which we'll give up waiting for a routes cooldown
to expire, and immedietly return.
Returns
-------
bool
False if the timeout period expired before the route finished cooling
down.
"""
return self._check(None, timeout) and self._check(route, timeout)
@ -111,8 +150,13 @@ class RateLimiter(object):
Updates the given routes state with the rate-limit headers inside the
response from a previous call to the route.
:param route: route to update
:param response: requests response to update the route with
Parameters
---------
route : tuple(HTTPMethod, str)
The route that will be updated.
response : :class:`requests.Response`
The response object for the last request to the route, whose headers
will be used to update the routes rate limit state.
"""
if 'X-RateLimit-Global' in response.headers:
route = None

152
disco/bot/bot.py

@ -6,36 +6,50 @@ from disco.bot.command import CommandEvent
class BotConfig(object):
"""
An object which specifies the runtime configuration for a Bot.
:ivar str token: Authentication token
:ivar bool commands_enabled: whether to enable the command parsing functionality
of the bot
:ivar bool command_require_mention: whether commands require a mention to be
triggered
:ivar dict command_mention_rules: a dictionary of rules about what types of
mentions will trigger a command. A string/bool mapping containing 'here',
'everyone', 'role', and 'user'. If set to false, the mention type will
not trigger commands.
:ivar str command_prefix: prefix required to trigger a command
:ivar bool command_allow_edit: whether editing the last-sent message in a channel,
which did not previously trigger a command, will cause the bot to recheck
the message contents and possibly trigger a command.
:ivar function plugin_config_provider: an optional function which when called
with a plugin name, returns relevant configuration for it.
An object which is used to configure and define the runtime configuration for
a bot.
Attributes
----------
token : str
The authentication token for this bot. This is passed on to the
:class:`disco.client.DiscoClient` without any validation.
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
interaction.
commands_require_mention : bool
Whether messages must mention the bot to be considered for command parsing.
commands_mention_rules : dict(str, bool)
A dictionary describing what mention types can be considered a mention
of the bot when using :attr:`commands_require_mention`. This dictionary
can contain the following keys: `here`, `everyone`, `role`, `user`. When
a keys value is set to true, the mention type will be considered for
command parsing.
commands_prefix : str
A string prefix that is required for a message to be considered for
command parsing.
commands_allow_edit : bool
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.
plugin_config_provider : Optional[function]
If set, this function will be called before loading a plugin, with the
plugins name. Its expected to return a type of configuration object the
plugin understands.
"""
token = None
commands_enabled = True
command_require_mention = True
command_mention_rules = {
commands_require_mention = True
commands_mention_rules = {
# 'here': False,
'everyone': False,
'role': True,
'user': True,
}
command_prefix = ''
command_allow_edit = True
commands_prefix = ''
commands_allow_edit = True
plugin_config_provider = None
@ -45,11 +59,24 @@ class Bot(object):
Disco's implementation of a simple but extendable Discord bot. Bots consist
of a set of plugins, and a Disco client.
:param client: the client this bot should use for its Discord connection
:param config: a :class:`BotConfig` instance
:ivar dict plugins: string -> :class:`disco.bot.plugin.Plugin` mapping of
all loaded plugins
Parameters
----------
client : Optional[:class:`disco.client.DiscoClient`]
The client this bot should utilize for its connection. If not provided,
will create a new :class:`disco.client.DiscoClient` with the token inside
the bot config (:class:`BotConfig`)
config : Optional[:class:`BotConfig`]
The configuration to use for this bot. If not provided will use the defaults
inside of :class:`BotConfig`.
Attributes
----------
client : `disco.client.DiscoClient`
The client instance for this bot.
config : `BotConfig`
The bot configuration instance for this bot.
plugins : dict(str, :class:`disco.bot.plugin.Plugin`)
Any plugins this bot has loaded
"""
def __init__(self, client=None, config=None):
self.client = client or DiscoClient(config.token)
@ -61,7 +88,7 @@ class Bot(object):
if self.config.commands_enabled:
self.client.events.on('MessageCreate', self.on_message_create)
if self.config.command_allow_edit:
if self.config.commands_allow_edit:
self.client.events.on('MessageUpdate', self.on_message_update)
# Stores the last message for every single channel
@ -73,10 +100,15 @@ class Bot(object):
@classmethod
def from_cli(cls, *plugins):
"""
Creates a new instance of the bot using the Disco-CLI utility, and a set
of passed-in plugin classes.
Creates a new instance of the bot using the utilities inside of the
:mod:`disco.cli` module. Allows passing in a set of uninitialized
plugin classes to load.
Parameters
---------
plugins : Optional[list(:class:`disco.bot.plugin.Plugin`)]
Any plugins to load after creating the new bot instance
:param plugins: plugins to load after creaing the Bot instance
"""
from disco.cli import disco_main
inst = cls(disco_main())
@ -107,15 +139,26 @@ class Bot(object):
def get_commands_for_message(self, msg):
"""
Generator of all commands a given message triggers.
Generator of all commands that a given message object triggers, based on
the bots plugins and configuration.
Parameters
---------
msg : :class:`disco.types.message.Message`
The message object to parse and find matching commands for
Yields
-------
tuple(:class:`disco.bot.command.Command`, `re.MatchObject`)
All commands the message triggers
"""
content = msg.content
if self.config.command_require_mention:
if self.config.commands_require_mention:
match = any((
self.config.command_mention_rules['user'] and msg.is_mentioned(self.client.state.me),
self.config.command_mention_rules['everyone'] and msg.mention_everyone,
self.config.command_mention_rules['role'] and any(map(msg.is_mentioned,
self.config.commands_mention_rules['user'] and msg.is_mentioned(self.client.state.me),
self.config.commands_mention_rules['everyone'] and msg.mention_everyone,
self.config.commands_mention_rules['role'] and any(map(msg.is_mentioned,
msg.guild.get_member(self.client.state.me).roles
))))
@ -124,10 +167,10 @@ class Bot(object):
content = msg.without_mentions.strip()
if self.config.command_prefix and not content.startswith(self.config.command_prefix):
if self.config.commands_prefix and not content.startswith(self.config.commands_prefix):
raise StopIteration
else:
content = content[len(self.config.command_prefix):]
content = content[len(self.config.commands_prefix):]
if not self.command_matches_re or not self.command_matches_re.match(content):
raise StopIteration
@ -139,11 +182,18 @@ class Bot(object):
def handle_message(self, msg):
"""
Attempts to handle a newely created or edited message in the context of
Attempts to handle a newly created or edited message in the context of
command parsing/triggering. Calls all relevant commands the message triggers.
:returns: whether any commands where successfully triggered
:rtype: bool
Parameters
---------
msg : :class:`disco.types.message.Message`
The newly created or updated message object to parse/handle.
Returns
-------
bool
whether any commands where successfully triggered by the message
"""
commands = list(self.get_commands_for_message(msg))
@ -156,13 +206,13 @@ class Bot(object):
return False
def on_message_create(self, event):
if self.config.command_allow_edit:
if self.config.commands_allow_edit:
self.last_message_cache[event.message.channel_id] = (event.message, False)
self.handle_message(event.message)
def on_message_update(self, event):
if self.config.command_allow_edit:
if self.config.commands_allow_edit:
obj = self.last_message_cache.get(event.message.channel_id)
if not obj:
return
@ -176,8 +226,12 @@ class Bot(object):
def add_plugin(self, cls):
"""
Adds and loads a given plugin, based on its class (which must be a subclass
of :class:`disco.bot.plugin.Plugin`).
Adds and loads a plugin, based on its class.
Parameters
----------
cls : subclass of :class:`disco.bot.plugin.Plugin`
Plugin class to initialize and load.
"""
if cls.__name__ in self.plugins:
raise Exception('Cannot add already added plugin: {}'.format(cls.__name__))
@ -190,8 +244,12 @@ class Bot(object):
def rmv_plugin(self, cls):
"""
Unloads and removes a given plugin, based on its class (which must be a
sub class of :class:`disco.bot.plugin.Plugin`).
Unloads and removes a plugin based on its class.
Parameters
----------
cls : subclass of :class:`disco.bot.plugin.Plugin`
Plugin class to unload and remove.
"""
if cls.__name__ not in self.plugins:
raise Exception('Cannot remove non-existant plugin: {}'.format(cls.__name__))
@ -203,6 +261,6 @@ class Bot(object):
def run_forever(self):
"""
Runs this bot forever
Runs this bots core loop forever
"""
self.client.run_forever()

47
disco/bot/command.py

@ -13,12 +13,20 @@ class CommandEvent(object):
about the message, command, and parsed arguments (along with shortcuts to
message information).
:ivar Command command: the command this event was created for (e.g. triggered command)
:ivar Message msg: the message object which triggered the command
:ivar re.MatchObject match: the regex match object for the command
:ivar string name: the name of the command (or alias) which was triggered
:ivar list args: any arguments passed to the command
Attributes
---------
command : :class:`Command`
The command this event was created for (aka the triggered command).
msg : :class:`disco.types.message.Message`
The message object which triggered this command.
match : :class:`re.MatchObject`
The regex match object for the command.
name : str
The command name (or alias) which was triggered by the command
args : list(str)
Arguments passed to the command
"""
def __init__(self, command, msg, match):
self.command = command
self.msg = msg
@ -67,13 +75,22 @@ class Command(object):
An object which defines and handles the triggering of a function based on
user input (aka a command).
:ivar disco.bot.plugin.Plugin plugin: the plugin this command is part of
:ivar function func: the function this command is attached too
:ivar str trigger: the primary trigger (aka name) of this command
:ivar str args: argument specification for this command
:ivar list aliases: aliases this command also responds too
:ivar str group: grouping this command is under
:ivar bool is_regex: whether this command is triggered as a regex
Attributes
----------
plugin : :class:`disco.bot.plugin.Plugin`
The plugin this command is a member of.
func : function
The function which is called when this command is triggered.
trigger : str
The primary trigger (aka name).
args : Optional[str]
The argument format specification.
aliases : Optional[list(str)]
List of trigger aliases.
group : Optional[str]
The group this command is a member of.
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):
self.plugin = plugin
@ -107,8 +124,10 @@ class Command(object):
Handles the execution of this command given a :class:`CommandEvent`
object.
:returns: whether this command was successful
:rtype: bool
Returns
-------
bool
Whether this command was sucessful
"""
if len(event.args) < self.args.required_length:
raise CommandError('{} requires {} arguments (passed {})'.format(

32
disco/bot/parser.py

@ -25,10 +25,16 @@ class Argument(object):
"""
A single argument, which is normally the member of a :class:`ArgumentSet`.
:ivar str name: name of this argument
:ivar int count: the number of actual raw arguments which compose this argument
:ivar bool required: whether this argument is required
:ivar list types: the types that this argument can be
Attributes
----------
name : str
The name of this argument.
count : int
The number of raw arguments that compose this argument.
required : bool
Whether this is a required argument.
types : list(type)
Types this argument supports.
"""
def __init__(self, raw):
self.name = None
@ -73,8 +79,12 @@ class ArgumentSet(object):
"""
A set of :class:`Argument` instances which forms a larger argument specification
:ivar list args: list of :class:`Argument` instances for this set
:ivar dict types: dict of all possible types
Attributes
----------
args : list(:class:`Argument`)
All arguments that are a member of this set.
types : dict(str, type)
All types supported by this ArgumentSet.
"""
def __init__(self, args=None, custom_types=None):
self.args = args or []
@ -99,10 +109,12 @@ class ArgumentSet(object):
"""
Attempts to convert a value to one or more types.
:param types: ordered list of types to try conversion to
:param value: the value to attempt conversion on
:type types: list
:param value: string
Parameters
----------
types : list(type)
List of types to attempt conversion with.
value : str
The string value to attempt conversion on.
"""
for typ_name in types:
typ = self.types.get(typ_name)

44
disco/bot/plugin.py

@ -83,13 +83,23 @@ class Plugin(LoggingClass, PluginDeco):
"""
A plugin is a set of listeners/commands which can be loaded/unloaded by a bot.
:param disco.bot.Bot bot: the bot this plugin is loaded under
:param config: a untyped object containing configuration for this plugin
:ivar disco.client.DiscoClient client: an alias to the client
:ivar disco.state.State state: an alias to the client state
:ivar list listeners: all bound listeners for this plugin
:ivar dict commands: all bound commands for this plugin
Parameters
----------
bot : :class:`disco.bot.Bot`
The bot this plugin is a member of.
config : any
The configuration data for this plugin.
Attributes
----------
client : :class:`disco.client.DiscoClient`
An alias to the client the bot is running with.
state : :class:`disco.state.State`
An alias to the state object for the client.
listeners : list
List of all bound listeners this plugin owns.
commands : list(:class:`disco.bot.command.Command`)
List of all commands this plugin owns.
"""
def __init__(self, bot, config):
super(Plugin, self).__init__()
@ -149,8 +159,12 @@ class Plugin(LoggingClass, PluginDeco):
"""
Registers a listener
:param func: function to be called
:param name: name of event to listen for
Parameters
----------
func : function
The function to be registered.
name : string
Name of event to listen for.
"""
func = functools.partial(self._dispatch, 'listener', func)
self.listeners.append(self.bot.client.events.on(name, func))
@ -159,9 +173,15 @@ class Plugin(LoggingClass, PluginDeco):
"""
Registers a command
:param func: function to be called
:param args: args to be passed to the :class:`Command` object
:param kwargs: kwargs to be passed to the :class:`Command` object
Parameters
----------
func : function
The function to be registered.
args
Arguments to pass onto the :class:`disco.bot.command.Command` object.
kwargs
Keyword arguments to pass onto the :class:`disco.bot.command.Command`
object.
"""
wrapped = functools.partial(self._dispatch, 'command', func)
self.commands[func.__name__] = Command(self, wrapped, *args, **kwargs)

13
disco/cli.py

@ -1,3 +1,7 @@
"""
The CLI module is a small utility that can be used as an easy entry point for
creating and running bots/clients.
"""
from __future__ import print_function
import logging
@ -15,6 +19,15 @@ logging.basicConfig(level=logging.INFO)
def disco_main():
"""
Creates an argument parser and parses a standard set of command line arguments,
creating a new :class:`DiscoClient`.
Returns
-------
:class:`DiscoClient`
A new DiscoClient from the provided command line arguments
"""
args = parser.parse_args()
from disco.util.token import is_valid_token

54
disco/client.py

@ -11,6 +11,36 @@ log = logging.getLogger(__name__)
class DiscoClient(object):
"""
The DiscoClient represents the base entry point to utilizing the Discord API
through disco. It wraps the functionality of both the REST API, and the realtime
secure websocket gateway.
Parameters
----------
token : str
The Discord authentication token which is used for both the :class:`APIClient`
and the :class:`GatewayClient`. This token can be validated before being
passed in, by using the :func:`disco.util.token.is_valid_token` function.
sharding : Optional[dict(str, int)]
A dictionary containing two pairs with information that is used to control
the sharding behavior of the :class:`GatewayClient`. By setting the `number`
key, the current shard ID can be controlled. While when setting the `total`
key, the total number of running shards can be set.
Attributes
----------
events : :class:`Emitter`
An emitter which emits Gateway events
packets : :class:`Emitter`
An emitter which emits Gateway packets
state : :class:`State`
The state tracking object
api : :class:`APIClient`
The API client
gw : :class:`GatewayClient`
The gateway client
"""
def __init__(self, token, sharding=None):
self.log = log
self.token = token
@ -25,18 +55,26 @@ class DiscoClient(object):
@classmethod
def from_cli(cls, args):
inst = cls(args.token)
inst.set_shard(args.shard_id, args.shard_count)
"""
Create a new client from a argparse command line argument object, usually
generated from the :func:`disco_main` function.
"""
inst = cls(
token=args.token,
sharding={
'number': args.shard_id,
'total': args.shard_count,
})
return inst
def set_shard(self, shard_number, shard_count):
self.sharding = {
'number': shard_number,
'total': shard_count,
}
def run(self):
"""
Run the client (e.g. the :class:`GatewayClient`) in a new greenlet
"""
return gevent.spawn(self.gw.run)
def run_forever(self):
"""
Run the client (e.g. the :class:`GatewayClient`) in the current greenlet
"""
return self.gw.run()

4
disco/gateway/client.py

@ -7,11 +7,11 @@ from disco.util.json import loads, dumps
from disco.util.websocket import WebsocketProcessProxy
from disco.util.logging import LoggingClass
GATEWAY_VERSION = 6
TEN_MEGABYTES = 10490000
class GatewayClient(LoggingClass):
GATEWAY_VERSION = 6
MAX_RECONNECTS = 5
def __init__(self, client):
@ -90,7 +90,7 @@ class GatewayClient(LoggingClass):
def connect_and_run(self):
if not self._cached_gateway_url:
self._cached_gateway_url = self.client.api.gateway(version=GATEWAY_VERSION, encoding='json')
self._cached_gateway_url = self.client.api.gateway(version=self.GATEWAY_VERSION, encoding='json')
self.log.info('Opening websocket connection to URL `%s`', self._cached_gateway_url)
self.ws = WebsocketProcessProxy(self._cached_gateway_url)

83
disco/state.py

@ -5,18 +5,77 @@ from weakref import WeakValueDictionary
from disco.gateway.packets import OPCode
StackMessage = namedtuple('StackMessage', ['id', 'channel_id', 'author_id'])
class StackMessage(namedtuple('StackMessage', ['id', 'channel_id', 'author_id'])):
"""
A message stored on a stack inside of the state object, used for tracking
previously sent messages in channels.
Attributes
---------
id : snowflake
the id of the message
channel_id : snowflake
the id of the channel this message was sent in
author_id : snowflake
the id of the author of this message
"""
class StateConfig(object):
# Whether to keep a buffer of messages
"""
A configuration object for determining how the State tracking behaves.
Attributes
----------
track_messages : bool
Whether the state store should keep a buffer of previously sent messages.
Message tracking allows for multiple higher-level shortcuts and can be
highly useful when developing bots that need to delete their own messages.
Message tracking is implemented using a deque and a namedtuple, meaning
it should generally not have a high impact on memory, however users who
find they do not need and may be experiencing memory pressure can disable
this feature entirely using this attribute.
track_messages_size : int
The size of the deque for each channel. Using this you can calculate the
total number of possible :class:`StackMessage` objects kept in memory,
using: `total_mesages_size * total_channels`. This can be tweaked based
on usage to help prevent memory pressure.
"""
track_messages = True
# The number maximum number of messages to store
track_messages_size = 100
class State(object):
"""
The State class is used to track global state based on events emitted from
the :class:`GatewayClient`. State tracking is a core component of the Disco
client, providing the mechanism for most of the higher-level utility functions.
Attributes
----------
EVENTS : list(str)
A list of all events the State object binds too.
client : :class:`disco.client.DiscoClient`
The DiscoClient instance this state is attached too
config : :class:`StateConfig`
The configuration for this state instance
me : :class:`disco.types.user.User`
The currently logged in user
dms : dict(snowflake, :class:`disco.types.channel.Channel`)
Mapping of all known DM Channels
guilds : dict(snowflake, :class:`disco.types.guild.Guild`)
Mapping of all known/loaded Guilds
channels : dict(snowflake, :class:`disco.types.channel.Channel`)
Weak mapping of all known/loaded Channels
users : dict(snowflake, :class:`disco.types.user.User`)
Weak mapping of all known/loaded Users
voice_states : dict(str, :class:`disco.types.voice.VoiceState`)
Weak mapping of all known/active Voice States
messages : Optional[dict(snowflake, :class:`deque`)]
Mapping of channel ids to deques containing :class:`StackMessage` objects
"""
EVENTS = [
'Ready', 'GuildCreate', 'GuildUpdate', 'GuildDelete', 'GuildMemberAdd', 'GuildMemberRemove',
'GuildMemberUpdate', 'GuildMembersChunk', 'GuildRoleCreate', 'GuildRoleUpdate', 'GuildRoleDelete',
@ -26,27 +85,37 @@ class State(object):
def __init__(self, client, config=None):
self.client = client
self.config = config or StateConfig()
self.listeners = []
self.me = None
self.dms = {}
self.guilds = {}
self.channels = WeakValueDictionary()
self.users = WeakValueDictionary()
self.voice_states = WeakValueDictionary()
self.messages = defaultdict(lambda: deque(maxlen=self.config.track_messages_size))
# If message tracking is enabled, listen to those events
if self.config.track_messages:
self.messages = defaultdict(lambda: deque(maxlen=self.config.track_messages_size))
self.EVENTS += ['MessageCreate', 'MessageDelete']
# The bound listener objects
self.listeners = []
self.bind()
def unbind(self):
"""
Unbinds all bound event listeners for this state object
"""
map(lambda k: k.unbind(), self.listeners)
self.listeners = []
def bind(self):
"""
Binds all events for this state object, storing the listeners for later
unbinding.
"""
assert not len(self.listeners), 'Binding while already bound is dangerous'
for event in self.EVENTS:
func = 'on_' + inflection.underscore(event)
self.listeners.append(self.client.events.on(event, getattr(self, func)))

128
disco/types/channel.py

@ -24,6 +24,21 @@ PermissionOverwriteType = Enum(
class PermissionOverwrite(BaseType):
"""
A PermissionOverwrite for a :class:`Channel`
Attributes
----------
id : snowflake
The overwrite ID
type : :const:`disco.types.channel.PermissionsOverwriteType`
The overwrite type
allowed : :class:`PermissionValue`
All allowed permissions
denied : :class:`PermissionValue`
All denied permissions
"""
id = skema.SnowflakeType()
type = skema.StringType(choices=PermissionOverwriteType.ALL_VALUES)
@ -32,6 +47,30 @@ class PermissionOverwrite(BaseType):
class Channel(BaseType, Permissible):
"""
Represents a Discord Channel
Attributes
----------
id : snowflake
The channel ID.
guild_id : Optional[snowflake]
The guild id this channel is part of.
name : str
The channels name.
topic : str
The channels topic.
position : int
The channels position.
bitrate : int
The channels bitrate.
recipients: list(:class:`disco.types.user.User`)
Members of this channel (if this is a DM channel).
type : :const:`ChannelType`
The type of this channel.
overwrites : dict(snowflake, :class:`disco.types.channel.PermissionOverwrite`)
Channel permissions overwrites.
"""
id = skema.SnowflakeType()
guild_id = skema.SnowflakeType(required=False)
@ -41,12 +80,20 @@ class Channel(BaseType, Permissible):
position = skema.IntType()
bitrate = skema.IntType(required=False)
recipient = skema.ModelType(User, required=False)
recipients = skema.ListType(skema.ModelType(User))
type = skema.IntType(choices=ChannelType.ALL_VALUES)
overwrites = ListToDictType('id', skema.ModelType(PermissionOverwrite), stored_name='permission_overwrites')
def get_permissions(self, user):
"""
Get the permissions a user has in the channel
Returns
-------
:class:`disco.types.permissions.PermissionValue`
Computed permission value for the user.
"""
if not self.guild_id:
return Permissions.ADMINISTRATOR
@ -64,49 +111,125 @@ class Channel(BaseType, Permissible):
@property
def is_guild(self):
"""
Whether this channel belongs to a guild
"""
return self.type in (ChannelType.GUILD_TEXT, ChannelType.GUILD_VOICE)
@property
def is_dm(self):
"""
Whether this channel is a DM (does not belong to a guild)
"""
return self.type in (ChannelType.DM, ChannelType.GROUP_DM)
@property
def is_voice(self):
"""
Whether this channel supports voice
"""
return self.type in (ChannelType.GUILD_VOICE, ChannelType.GROUP_DM)
@property
def last_message_id(self):
"""
Returns the ID of the last message sent in this channel
"""
if self.id not in self.client.state.messages:
return self._last_message_id
return self.client.state.messages[self.id][-1].id
@property
def messages(self):
"""
a default :class:`MessageIterator` for the channel
"""
return self.messages_iter()
def messages_iter(self, **kwargs):
"""
Creates a new :class:`MessageIterator` for the channel with the given
keyword arguments
"""
return MessageIterator(self.client, self.id, before=self.last_message_id, **kwargs)
@cached_property
def guild(self):
"""
Guild this channel belongs to (if relevant)
"""
return self.client.state.guilds.get(self.guild_id)
def get_invites(self):
"""
Returns
-------
list(:class:`disco.types.invite.Invite`)
All invites for this channel.
"""
return self.client.api.channels_invites_list(self.id)
def get_pins(self):
"""
Returns
-------
list(:class:`disco.types.message.Message`)
All pinned messages for this channel.
"""
return self.client.api.channels_pins_list(self.id)
def send_message(self, content, nonce=None, tts=False):
"""
Send a message in this channel
Parameters
----------
content : str
The message contents to send.
nonce : Optional[snowflake]
The nonce to attach to the message.
tts : Optional[bool]
Whether this is a TTS message.
Returns
-------
:class:`disco.types.message.Message`
The created message.
"""
return self.client.api.channels_messages_create(self.id, content, nonce, tts)
def connect(self, *args, **kwargs):
"""
Connect to this channel over voice
"""
assert self.is_voice, 'Channel must support voice to connect'
vc = VoiceClient(self)
vc.connect(*args, **kwargs)
return vc
class MessageIterator(object):
"""
An iterator which supports scanning through the messages for a channel.
Parameters
----------
client : :class:`disco.client.DiscoClient`
The disco client instance to use when making requests.
channel : `Channel`
The channel to iterate within.
direction : :attr:`MessageIterator.Direction`
The direction in which this iterator will move.
bulk : bool
If true, this iterator will yield messages in list batches, otherwise each
message will be yield individually.
before : snowflake
The message to begin scanning at.
after : snowflake
The message to begin scanning at.
chunk_size : int
The number of messages to request per API call.
"""
Direction = Enum('UP', 'DOWN')
def __init__(self, client, channel, direction=Direction.UP, bulk=False, before=None, after=None, chunk_size=100):
@ -128,6 +251,9 @@ class MessageIterator(object):
raise Exception('Must specify either before or after for downward seeking')
def fill(self):
"""
Fills the internal buffer up with :class:`disco.types.message.Message` objects from the API
"""
self._buffer = self.client.api.channels_messages_list(
self.channel,
before=self.before,

145
disco/types/guild.py

@ -12,6 +12,22 @@ from disco.types.channel import Channel
class Emoji(BaseType):
"""
An emoji object
Attributes
----------
id : snowflake
The ID of this emoji.
name : str
The name of this emoji.
require_colons : bool
Whether this emoji requires colons to use.
managed : bool
Whether this emoji is managed by an integration.
roles : list(snowflake)
Roles this emoji is attached to.
"""
id = skema.SnowflakeType()
name = skema.StringType()
require_colons = skema.BooleanType()
@ -20,6 +36,26 @@ class Emoji(BaseType):
class Role(BaseType):
"""
A role object
Attributes
----------
id : snowflake
The role ID.
name : string
The role name.
hoist : bool
Whether this role is hoisted (displayed separately in the sidebar).
managed : bool
Whether this role is managed by an integration.
color : int
The RGB color of this role.
permissions : :class:`disco.types.permissions.PermissionsValue`
The permissions this role grants.
position : int
The position of this role in the hierarchy.
"""
id = skema.SnowflakeType()
name = skema.StringType()
hoist = skema.BooleanType()
@ -30,6 +66,24 @@ class Role(BaseType):
class GuildMember(BaseType):
"""
A GuildMember object
Attributes
----------
user : :class:`disco.types.user.User`
The user object of this member.
guild_id : snowflake
The guild this member is part of.
mute : bool
Whether this member is server voice-muted.
deaf : bool
Whether this member is server voice-deafend.
joined_at : datetime
When this user joined the guild.
roles : list(snowflake)
Roles this member is part of.
"""
user = skema.ModelType(User)
guild_id = skema.SnowflakeType(required=False)
mute = skema.BooleanType()
@ -38,20 +92,84 @@ class GuildMember(BaseType):
roles = skema.ListType(skema.SnowflakeType())
def get_voice_state(self):
"""
Returns
-------
Optional[:class:`disco.types.voice.VoiceState`]
Returns the voice state for the member if they are currently connected
to the guilds voice server.
"""
return self.guild.get_voice_state(self)
def kick(self):
"""
Kicks the member from the guild.
"""
self.client.api.guilds_members_kick(self.guild.id, self.user.id)
def ban(self, delete_message_days=0):
"""
Bans the member from the guild.
Args
----
delete_message_days : int
The number of days to retroactively delete messages for.
"""
self.client.api.guilds_bans_create(self.guild.id, self.user.id, delete_message_days)
@property
def id(self):
"""
Alias to the guild members user id
"""
return self.user.id
class Guild(BaseType, Permissible):
"""
A guild object
Attributes
----------
id : snowflake
The id of this guild.
owner_id : snowflake
The id of the owner.
afk_channel_id : snowflake
The id of the afk channel.
embed_channel_id : snowflake
The id of the embed channel.
name : str
Guilds name.
icon : str
Guilds icon (as PNG binary data).
splash : str
Guilds splash image (as PNG binary data).
region : str
Voice region.
afk_timeout : int
Delay after which users are automatically moved to the afk channel.
embed_enabled : bool
Whether the guilds embed is enabled.
verification_level : int
The verification level used by the guild.
mfa_level : int
The MFA level used by the guild.
features : list(str)
Extra features enabled for this guild.
members : dict(snowflake, :class:`GuildMember`)
All of the guilds members.
channels : dict(snowflake, :class:`disco.types.channel.Channel`)
All of the guilds channels.
roles : dict(snowflake, :class:`Role`)
All of the guilds roles.
emojis : dict(snowflake, :class:`Emoji`)
All of the guilds emojis.
voice_states : dict(str, :class:`disco.types.voice.VoiceState`)
All of the guilds voice states.
"""
id = skema.SnowflakeType()
owner_id = skema.SnowflakeType()
@ -77,6 +195,14 @@ class Guild(BaseType, Permissible):
voice_states = ListToDictType('session_id', skema.ModelType(VoiceState))
def get_permissions(self, user):
"""
Get the permissions a user has in this guild.
Returns
-------
:class:`disco.types.permissions.PermissionValue`
Computed permission value for the user.
"""
if self.owner_id == user.id:
return PermissionValue(Permissions.ADMINISTRATOR)
@ -89,6 +215,15 @@ class Guild(BaseType, Permissible):
return value
def get_voice_state(self, user):
"""
Attempt to get a voice state for a given user (who should be a member of
this guild).
Returns
-------
:class:`disco.types.voice.VoiceState`
The voice state for the user in this guild.
"""
user = to_snowflake(user)
for state in self.voice_states.values():
@ -96,13 +231,21 @@ class Guild(BaseType, Permissible):
return state
def get_member(self, user):
"""
Attempt to get a member from a given user.
Returns
-------
:class:`GuildMember`
The guild member object for the given user.
"""
user = to_snowflake(user)
if user not in self.members:
try:
self.members[user] = self.client.api.guilds_members_get(self.id, user)
except APIException:
pass
return
return self.members.get(user)

24
disco/types/invite.py

@ -8,6 +8,30 @@ from disco.types.channel import Channel
class Invite(BaseType):
"""
An invite object
Attributes
----------
code : str
The invite code.
inviter : :class:`disco.types.user.User`
The user who created this invite.
guild : :class:`disco.types.guild.Guild`
The guild this invite is for.
channel : :class:`disco.types.channel.Channel`
The channel this invite is for.
max_age : int
The time after this invites creation at which it expires.
max_uses : int
The maximum number of uses.
uses : int
The current number of times the invite was used.
temporary : bool
Whether this invite only grants temporary memborship.
created_at : datetime
When this invite was created.
"""
code = skema.StringType()
inviter = skema.ModelType(User)

139
disco/types/message.py

@ -9,6 +9,20 @@ from disco.types.user import User
class MessageEmbed(BaseType):
"""
Message embed object
Attributes
----------
title : str
Title of the embed.
type : str
Type of the embed.
description : str
Description of the embed.
url : str
URL of the embed.
"""
title = skema.StringType()
type = skema.StringType()
description = skema.StringType()
@ -16,6 +30,26 @@ class MessageEmbed(BaseType):
class MessageAttachment(BaseType):
"""
Message attachment object
Attributes
----------
id : snowflake
The id of this attachment.
filename : str
The filename of this attachment.
url : str
The URL of this attachment.
proxy_url : str
The URL to proxy through when downloading the attachment.
size : int
Size of the attachment.
height : int
Height of the attachment.
width : int
Width of the attachment.
"""
id = skema.SnowflakeType()
filename = skema.StringType()
url = skema.StringType()
@ -26,6 +60,40 @@ class MessageAttachment(BaseType):
class Message(BaseType):
"""
Represents a Message created within a Channel on Discord.
Attributes
----------
id : snowflake
The ID of this message.
channel_id : snowflake
The channel ID this message was sent in.
author : :class:`disco.types.user.User`
The author of this message.
content : str
The unicode contents of this message.
nonce : str
The nonce of this message.
timestamp : datetime
When this message was created.
edited_timestamp : Optional[datetime]
When this message was last edited.
tts : bool
Whether this is a TTS (text-to-speech) message.
mention_everyone : bool
Whether this message has an @everyone which mentions everyone.
pinned : bool
Whether this message is pinned in the channel.
mentions : dict(snowflake, :class:`disco.types.user.User`)
All users mentioned within this message.
mention_roles : list(snowflake)
All roles mentioned within this message.
embeds : list(:class:`MessageEmbed`)
All embeds for this message.
attachments : list(:class:`MessageAttachment`)
All attachments for this message.
"""
id = skema.SnowflakeType()
channel_id = skema.SnowflakeType()
@ -52,32 +120,103 @@ class Message(BaseType):
@cached_property
def guild(self):
"""
Returns
-------
:class:`disco.types.guild.Guild`
The guild (if applicable) this message was created in.
"""
return self.channel.guild
@cached_property
def channel(self):
"""
Returns
-------
:class:`disco.types.channel.Channel`
The channel this message was created in.
"""
return self.client.state.channels.get(self.channel_id)
def reply(self, *args, **kwargs):
"""
Reply to this message (proxys arguments to
:func:`disco.types.channel.Channel.send_message`)
Returns
-------
:class:`Message`
The created message object.
"""
return self.channel.send_message(*args, **kwargs)
def edit(self, content):
"""
Edit this message
Args
----
content : str
The new edited contents of the message.
Returns
-------
:class:`Message`
The edited message object.
"""
return self.client.api.channels_messages_modify(self.channel_id, self.id, content)
def delete(self):
"""
Delete this message.
Returns
-------
:class:`Message`
The deleted message object.
"""
return self.client.api.channels_messages_delete(self.channel_id, self.id)
def is_mentioned(self, entity):
"""
Returns
-------
bool
Whether the give entity was mentioned.
"""
id = to_snowflake(entity)
return id in self.mentions or id in self.mention_roles
@cached_property
def without_mentions(self):
"""
Returns
-------
str
the message contents with all valid mentions removed.
"""
return self.replace_mentions(
lambda u: '',
lambda r: '')
def replace_mentions(self, user_replace, role_replace):
"""
Replaces user and role mentions with the result of a given lambda/function.
Args
----
user_replace : function
A function taking a single argument, the user object mentioned, and
returning a valid string.
role_replace : function
A function taking a single argument, the role ID mentioned, and
returning a valid string.
Returns
-------
str
The message contents with all valid mentions replaced.
"""
if not self.mentions and not self.mention_roles:
return

2
disco/voice/client.py

@ -86,7 +86,7 @@ class VoiceClient(LoggingClass):
def __init__(self, channel):
super(VoiceClient, self).__init__()
assert(channel.is_voice)
assert channel.is_voice, 'Cannot spawn a VoiceClient for a non-voice channel'
self.channel = channel
self.client = self.channel.client

150
docs/api.rst

@ -0,0 +1,150 @@
.. currentmodule:: disco
API Reference
=============
Version Information
-------------------
disco exports a top-level variable that can be used to introspect the current
version information for the installed package.
.. data:: VERSION
A string representation of the current version, in standard semantic
versioning format. E.g. ``'5.4.3-rc.2'``
DiscoClient
------------
.. autoclass:: disco.client.DiscoClient
:members:
State
-----
.. automodule:: disco.state
:members:
CLI
---
.. automodule:: disco.cli
:members:
Types
-----
Channel
~~~~~~~
.. automodule:: disco.types.channel
:members:
Guild
~~~~~
.. automodule:: disco.types.guild
:members:
Message
~~~~~~~
.. automodule:: disco.types.message
:members:
User
~~~~
.. automodule:: disco.types.user
:members:
Voice
~~~~~
.. automodule:: disco.types.voice
:members:
Invite
~~~~~~
.. automodule:: disco.types.invite
:members:
Permissions
~~~~~~~~~~~
.. automodule:: disco.types.permissions
:members:
Bot Toolkit
-----------
.. automodule:: disco.bot.bot
:members:
Plugins
~~~~~~~
.. automodule:: disco.bot.plugin
:members:
Commands
~~~~~~~~
.. automodule:: disco.bot.command
:members:
Command Argument Parser
~~~~~~~~~~~~~~~~~~~~~~~
.. automodule:: disco.bot.parser
:members:
Gateway API
-----------
GatewayClient
~~~~~~~~~~~~~
.. autoclass:: disco.gateway.client.GatewayClient
:members:
Gateway Events
~~~~~~~~~~~~~~
.. automodule:: disco.gateway.client.Events
:members:
REST API
--------
APIClient
~~~~~~~~~
.. autoclass:: disco.api.client.APIClient
:members:
:undoc-members:
HTTP Utilities
~~~~~~~~~~~~~~
.. autoclass:: disco.api.http.APIException
:members:
.. autoclass:: disco.api.http.HTTPClient
:members:
Ratelimit Utilities
~~~~~~~~~~~~~~~~~~~
.. autoclass:: disco.api.ratelimit.RouteState
:members:
.. autoclass:: disco.api.ratelimit.RateLimiter
:members:

1
docs/conf.py

@ -34,6 +34,7 @@ extensions = [
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
'sphinx.ext.githubpages',
'sphinx.ext.napoleon'
]
# Add any paths that contain templates here, relative to this directory.

38
docs/disco.api.rst

@ -1,38 +0,0 @@
disco.api package
=================
Submodules
----------
disco.api.client module
-----------------------
.. automodule:: disco.api.client
:members:
:undoc-members:
:show-inheritance:
disco.api.http module
---------------------
.. automodule:: disco.api.http
:members:
:undoc-members:
:show-inheritance:
disco.api.ratelimit module
--------------------------
.. automodule:: disco.api.ratelimit
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: disco.api
:members:
:undoc-members:
:show-inheritance:

46
docs/disco.bot.rst

@ -1,46 +0,0 @@
disco.bot package
=================
Submodules
----------
disco.bot.bot module
--------------------
.. automodule:: disco.bot.bot
:members:
:undoc-members:
:show-inheritance:
disco.bot.command module
------------------------
.. automodule:: disco.bot.command
:members:
:undoc-members:
:show-inheritance:
disco.bot.parser module
-----------------------
.. automodule:: disco.bot.parser
:members:
:undoc-members:
:show-inheritance:
disco.bot.plugin module
-----------------------
.. automodule:: disco.bot.plugin
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: disco.bot
:members:
:undoc-members:
:show-inheritance:

38
docs/disco.gateway.rst

@ -1,38 +0,0 @@
disco.gateway package
=====================
Submodules
----------
disco.gateway.client module
---------------------------
.. automodule:: disco.gateway.client
:members:
:undoc-members:
:show-inheritance:
disco.gateway.events module
---------------------------
.. automodule:: disco.gateway.events
:members:
:undoc-members:
:show-inheritance:
disco.gateway.packets module
----------------------------
.. automodule:: disco.gateway.packets
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: disco.gateway
:members:
:undoc-members:
:show-inheritance:

50
docs/disco.rst

@ -1,50 +0,0 @@
disco package
=============
Subpackages
-----------
.. toctree::
disco.api
disco.bot
disco.gateway
disco.types
disco.util
disco.voice
Submodules
----------
disco.cli module
----------------
.. automodule:: disco.cli
:members:
:undoc-members:
:show-inheritance:
disco.client module
-------------------
.. automodule:: disco.client
:members:
:undoc-members:
:show-inheritance:
disco.state module
------------------
.. automodule:: disco.state
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: disco
:members:
:undoc-members:
:show-inheritance:

78
docs/disco.types.rst

@ -1,78 +0,0 @@
disco.types package
===================
Submodules
----------
disco.types.base module
-----------------------
.. automodule:: disco.types.base
:members:
:undoc-members:
:show-inheritance:
disco.types.channel module
--------------------------
.. automodule:: disco.types.channel
:members:
:undoc-members:
:show-inheritance:
disco.types.guild module
------------------------
.. automodule:: disco.types.guild
:members:
:undoc-members:
:show-inheritance:
disco.types.invite module
-------------------------
.. automodule:: disco.types.invite
:members:
:undoc-members:
:show-inheritance:
disco.types.message module
--------------------------
.. automodule:: disco.types.message
:members:
:undoc-members:
:show-inheritance:
disco.types.permissions module
------------------------------
.. automodule:: disco.types.permissions
:members:
:undoc-members:
:show-inheritance:
disco.types.user module
-----------------------
.. automodule:: disco.types.user
:members:
:undoc-members:
:show-inheritance:
disco.types.voice module
------------------------
.. automodule:: disco.types.voice
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: disco.types
:members:
:undoc-members:
:show-inheritance:

62
docs/disco.util.rst

@ -1,62 +0,0 @@
disco.util package
==================
Submodules
----------
disco.util.cache module
-----------------------
.. automodule:: disco.util.cache
:members:
:undoc-members:
:show-inheritance:
disco.util.json module
----------------------
.. automodule:: disco.util.json
:members:
:undoc-members:
:show-inheritance:
disco.util.logging module
-------------------------
.. automodule:: disco.util.logging
:members:
:undoc-members:
:show-inheritance:
disco.util.token module
-----------------------
.. automodule:: disco.util.token
:members:
:undoc-members:
:show-inheritance:
disco.util.types module
-----------------------
.. automodule:: disco.util.types
:members:
:undoc-members:
:show-inheritance:
disco.util.websocket module
---------------------------
.. automodule:: disco.util.websocket
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: disco.util
:members:
:undoc-members:
:show-inheritance:

30
docs/disco.voice.rst

@ -1,30 +0,0 @@
disco.voice package
===================
Submodules
----------
disco.voice.client module
-------------------------
.. automodule:: disco.voice.client
:members:
:undoc-members:
:show-inheritance:
disco.voice.packets module
--------------------------
.. automodule:: disco.voice.packets
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: disco.voice
:members:
:undoc-members:
:show-inheritance:

19
docs/index.rst

@ -1,17 +1,22 @@
.. disco documentation master file, created by
sphinx-quickstart on Tue Oct 4 22:15:06 2016.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to disco's documentation!
disco
=================================
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
Contents:
---------
.. toctree::
:maxdepth: 2
tutorial
api
Indices and tables
==================

7
docs/modules.rst

@ -1,7 +0,0 @@
disco
=====
.. toctree::
:maxdepth: 4
disco

7
docs/setup.rst

@ -1,7 +0,0 @@
setup module
============
.. automodule:: setup
:members:
:undoc-members:
:show-inheritance:

5
docs/tutorial.rst

@ -0,0 +1,5 @@
.. currentmodule:: disco
Tutorial
========
Loading…
Cancel
Save