diff --git a/.gitignore b/.gitignore index 541f92c..356e9ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -# Add any directories, files, or patterns you don't want to be tracked by version control \ No newline at end of file +__pycache__/ +discord.py-rewrite/ +test.png +password.txt \ No newline at end of file diff --git a/Shitpost.py b/Shitpost.py index c8321b8..f7d493c 100644 --- a/Shitpost.py +++ b/Shitpost.py @@ -16,7 +16,7 @@ class Shitpost(object): def __check(self, ctx): #Todo: Is dit het shitpost kanaal? return True - @commands.command(pass_context=True, hidden=True) + @commands.command(pass_context=True, hidden=False) @asyncio.coroutine def mark(self, ctx, boven: str, onder: str): """Maakt een mark meem. @@ -42,7 +42,7 @@ class Shitpost(object): print("No permission to delete message") yield from ctx.channel.send(ctx.author.mention, embed=em) - @commands.command(pass_context=True, hidden=True) + @commands.command(pass_context=True, hidden=False) @asyncio.coroutine def eriku(self, ctx, boven: str, onder: str): """Maakt een erik meem. @@ -69,7 +69,7 @@ class Shitpost(object): yield from ctx.channel.send(ctx.author.mention, embed=em) - @commands.command(pass_context=True, hidden=True) + @commands.command(pass_context=True, hidden=False) @asyncio.coroutine def zout(self, ctx, boven: str, onder: str): """Maakt een martijn-zout meem. @@ -95,7 +95,7 @@ class Shitpost(object): print("No permission to delete message") yield from ctx.channel.send(ctx.author.mention, embed=em) - @commands.command(pass_context=True, hidden=True) + @commands.command(pass_context=True, hidden=False) @asyncio.coroutine def nihao(self,ctx): #yield from self.bot.say("Kankerlauw") diff --git a/discord.py-rewrite/.gitignore b/discord.py-rewrite/.gitignore deleted file mode 100644 index ed880af..0000000 --- a/discord.py-rewrite/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -*.json -*.pyc -*.log -docs/_build -*.buildinfo -*.mp3 -*.m4a -*.wav -*.png -*.jpg -*.flac diff --git a/discord.py-rewrite/LICENSE b/discord.py-rewrite/LICENSE deleted file mode 100644 index f7befea..0000000 --- a/discord.py-rewrite/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/discord.py-rewrite/MANIFEST.in b/discord.py-rewrite/MANIFEST.in deleted file mode 100644 index 4857737..0000000 --- a/discord.py-rewrite/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include README.md -include LICENSE -include requirements.txt -include discord/bin/*.dll diff --git a/discord.py-rewrite/README.rst b/discord.py-rewrite/README.rst deleted file mode 100644 index 825a9aa..0000000 --- a/discord.py-rewrite/README.rst +++ /dev/null @@ -1,106 +0,0 @@ -discord.py -========== - -.. image:: https://img.shields.io/pypi/v/discord.py.svg - :target: https://pypi.python.org/pypi/discord.py -.. image:: https://img.shields.io/pypi/pyversions/discord.py.svg - :target: https://pypi.python.org/pypi/discord.py - -discord.py is an API wrapper for Discord written in Python. - -This was written to allow easier writing of bots or chat logs. Make sure to familiarise yourself with the API using the `documentation `__. - -Breaking Changes ---------------- - -The discord API is constantly changing and the wrapper API is as well. There will be no effort to keep backwards compatibility in versions before ``v1.0.0``. - -I recommend that you follow the discussion in the `unofficial Discord API discord channel `__ and update your installation periodically. I will attempt to make note of breaking changes in the API channel so make sure to subscribe to library news by typing ``?sub news`` in the channel. - -Installing ----------- - -To install the library without full voice support, you can just run the following command: - -.. code:: sh - - python3 -m pip install -U discord.py - -Otherwise to get voice support you should run the following command: - -.. code:: sh - - python3 -m pip install -U discord.py[voice] - - -To install the development version, do the following: - -.. code:: sh - - python3 -m pip install -U https://github.com/Rapptz/discord.py/archive/master.zip#egg=discord.py[voice] - -or the more long winded from cloned source: - -.. code:: sh - - $ git clone https://github.com/Rapptz/discord.py - $ cd discord.py - $ python3 -m pip install -U .[voice] - -Please note that on Linux installing voice you must install the following packages via your favourite package manager (e.g. ``apt``, ``yum``, etc) before running the above command: - -* libffi-dev (or ``libffi-devel`` on some systems) -* python-dev (e.g. ``python3.5-dev`` for Python 3.5) - -Quick Example ------------- - -.. code:: py - - import discord - import asyncio - - class MyClient(discord.Client): - async def on_ready(self): - print('Logged in as') - print(self.user.name) - print(self.user.id) - print('------') - - async def on_message(self, message): - # don't respond to ourselves - if message.author == self.user: - return - if message.content.startswith('!test'): - counter = 0 - tmp = await message.channel.send('Calculating messages...') - async for msg in message.channel.history(limit=100): - if msg.author == message.author: - counter += 1 - - await tmp.edit(content='You have {} messages.'.format(counter)) - elif message.content.startswith('!sleep'): - with message.channel.typing(): - await asyncio.sleep(5.0) - await message.channel.send('Done sleeping.') - - client = MyClient() - client.run('token') - -Note that in Python 3.4 you use ``@asyncio.coroutine`` instead of ``async def`` and ``yield from`` instead of ``await``. - -You can find examples in the examples directory. - -Requirements ------------- - -* Python 3.4.2+ -* ``aiohttp`` library -* ``websockets`` library -* ``PyNaCl`` library (optional, for voice only) - - - On Linux systems this requires the ``libffi`` library. You can install in - debian based systems by doing ``sudo apt-get install libffi-dev``. - -Usually ``pip`` will handle these for you. - diff --git a/discord.py-rewrite/discord/__init__.py b/discord.py-rewrite/discord/__init__.py deleted file mode 100644 index 7973cd4..0000000 --- a/discord.py-rewrite/discord/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Discord API Wrapper -~~~~~~~~~~~~~~~~~~~ - -A basic wrapper for the Discord API. - -:copyright: (c) 2015-2017 Rapptz -:license: MIT, see LICENSE for more details. - -""" - -__title__ = 'discord' -__author__ = 'Rapptz' -__license__ = 'MIT' -__copyright__ = 'Copyright 2015-2017 Rapptz' -__version__ = '1.0.0a' - -from .client import Client, AppInfo -from .user import User, ClientUser, Profile -from .game import Game -from .emoji import Emoji, PartialReactionEmoji -from .channel import * -from .guild import Guild -from .relationship import Relationship -from .member import Member, VoiceState -from .message import Message, Attachment -from .errors import * -from .calls import CallMessage, GroupCall -from .permissions import Permissions, PermissionOverwrite -from .role import Role -from .file import File -from .colour import Color, Colour -from .invite import Invite -from .object import Object -from .reaction import Reaction -from . import utils, opus, compat, abc -from .enums import * -from collections import namedtuple -from .embeds import Embed -from .shard import AutoShardedClient -from .player import * -from .webhook import * -from .voice_client import VoiceClient -from .audit_logs import AuditLogChanges, AuditLogEntry, AuditLogDiff - -import logging - -VersionInfo = namedtuple('VersionInfo', 'major minor micro releaselevel serial') - -version_info = VersionInfo(major=1, minor=0, micro=0, releaselevel='alpha', serial=0) - -try: - from logging import NullHandler -except ImportError: - class NullHandler(logging.Handler): - def emit(self, record): - pass - -logging.getLogger(__name__).addHandler(NullHandler()) diff --git a/discord.py-rewrite/discord/__main__.py b/discord.py-rewrite/discord/__main__.py deleted file mode 100644 index d40d404..0000000 --- a/discord.py-rewrite/discord/__main__.py +++ /dev/null @@ -1,284 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import discord -import argparse -import sys -from pathlib import Path -import os -import re - -def core(parser, args): - pass - -bot_template = """#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -from discord.ext import commands -import discord -import config - -class Bot(commands.{base}): - def __init__(self, **kwargs): - super().__init__(command_prefix=commands.when_mentioned_or('{prefix}'), **kwargs) - for cog in config.cogs: - try: - self.load_extension(cog) - except Exception as e: - print('Could not load extension {{0}} due to {{1.__class__.__name__}}: {{1}}'.format(cog, e)) - - async def on_ready(self): - print('Logged on as {{0}} (ID: {{0.id}})'.format(self.user)) - - -bot = Bot() - -# write general commands here - -bot.run(config.token) -""" - -gitignore_template = """# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# Our configuration files -config.py -""" - -cog_template = '''# -*- coding: utf-8 -*- - -from discord.ext import commands -import discord - -class {name}: - """The description for {name} goes here.""" - - def __init__(self, bot): - self.bot = bot -{extra} -def setup(bot): - bot.add_cog({name}(bot)) -''' - -cog_extras = ''' - def __unload(self): - # clean up logic goes here - pass - - async def __local_check(self, ctx): - # checks that apply to every command in here - return True - - async def __global_check(self, ctx): - # checks that apply to every command to the bot - return True - - async def __global_check_once(self, ctx): - # check that apply to every command but is guaranteed to be called only once - return True - - async def __error(self, ctx, error): - # error handling to every command in here - pass - - async def __before_invoke(self, ctx): - # called before a command is called here - pass - - async def __after_invoke(self, ctx): - # called after a command is called here - pass - -''' - - -# certain file names and directory names are forbidden -# see: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx -# although some of this doesn't apply to Linux, we might as well be consistent -_base_table = { - '<': '-', - '>': '-', - ':': '-', - '"': '-', - # '/': '-', these are fine - # '\\': '-', - '|': '-', - '?': '-', - '*': '-', -} - -# -_base_table.update((chr(i), None) for i in range(32)) - -translation_table = str.maketrans(_base_table) - -def to_path(parser, name, *, replace_spaces=False): - if isinstance(name, Path): - return name - - if sys.platform == 'win32': - forbidden = ('CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', \ - 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9') - if len(name) <= 4 and name.upper() in forbidden: - parser.error('invalid directory name given, use a different one') - - name = name.translate(translation_table) - if replace_spaces: - name = name.replace(' ', '-') - return Path(name) - -def newbot(parser, args): - if sys.version_info < (3, 5): - parser.error('python version is older than 3.5, consider upgrading.') - - new_directory = to_path(parser, args.directory) / to_path(parser, args.name) - - # as a note exist_ok for Path is a 3.5+ only feature - # since we already checked above that we're >3.5 - try: - new_directory.mkdir(exist_ok=True, parents=True) - except OSError as e: - parser.error('could not create our bot directory ({})'.format(e)) - - cogs = new_directory / 'cogs' - - try: - cogs.mkdir(exist_ok=True) - init = cogs / '__init__.py' - init.touch() - except OSError as e: - print('warning: could not create cogs directory ({})'.format(e)) - - try: - with open(str(new_directory / 'config.py'), 'w', encoding='utf-8') as fp: - fp.write('token = "place your token here"\ncogs = []\n') - except OSError as e: - parser.error('could not create config file ({})'.format(e)) - - try: - with open(str(new_directory / 'bot.py'), 'w', encoding='utf-8') as fp: - base = 'Bot' if not args.sharded else 'AutoShardedBot' - fp.write(bot_template.format(base=base, prefix=args.prefix)) - except OSError as e: - parser.error('could not create bot file ({})'.format(e)) - - if not args.no_git: - try: - with open(str(new_directory / '.gitignore'), 'w', encoding='utf-8') as fp: - fp.write(gitignore_template) - except OSError as e: - print('warning: could not create .gitignore file ({})'.format(e)) - - print('successfully made bot at', new_directory) - -def newcog(parser, args): - if sys.version_info < (3, 5): - parser.error('python version is older than 3.5, consider upgrading.') - - cog_dir = to_path(parser, args.directory) - try: - cog_dir.mkdir(exist_ok=True) - except OSError as e: - print('warning: could not create cogs directory ({})'.format(e)) - - directory = cog_dir / to_path(parser, args.name) - directory = directory.with_suffix('.py') - try: - with open(str(directory), 'w', encoding='utf-8') as fp: - extra = cog_extras if args.full else '' - if args.class_name: - name = args.class_name - else: - name = str(directory.stem) - if '-' in name: - name = name.replace('-', ' ').title().replace(' ', '') - else: - name = name.title() - fp.write(cog_template.format(name=name, extra=extra)) - except OSError as e: - parser.error('could not create cog file ({})'.format(e)) - else: - print('successfully made cog at', directory) - -def add_newbot_args(subparser): - parser = subparser.add_parser('newbot', help='creates a command bot project quickly') - parser.set_defaults(func=newbot) - - parser.add_argument('name', help='the bot project name') - parser.add_argument('directory', help='the directory to place it in (default: .)', nargs='?', default=Path.cwd()) - parser.add_argument('--prefix', help='the bot prefix (default: $)', default='$', metavar='') - parser.add_argument('--sharded', help='whether to use AutoShardedBot', action='store_true') - parser.add_argument('--no-git', help='do not create a .gitignore file', action='store_true', dest='no_git') - -def add_newcog_args(subparser): - parser = subparser.add_parser('newcog', help='creates a new cog template quickly') - parser.set_defaults(func=newcog) - - parser.add_argument('name', help='the cog name') - parser.add_argument('directory', help='the directory to place it in (default: cogs)', nargs='?', default=Path('cogs')) - parser.add_argument('--class-name', help='the class name of the cog (default: )', dest='class_name') - parser.add_argument('--full', help='add all special methods as well', action='store_true') - -def parse_args(): - parser = argparse.ArgumentParser(prog='discord', description='Tools for helping with discord.py') - - version = 'discord.py v{0.__version__} for Python {1[0]}.{1[1]}.{1[2]}'.format(discord, sys.version_info) - parser.add_argument('-v', '--version', action='version', version=version, help='shows the library version') - parser.set_defaults(func=core) - - subparser = parser.add_subparsers(dest='subcommand', title='subcommands') - add_newbot_args(subparser) - add_newcog_args(subparser) - return parser, parser.parse_args() - -def main(): - parser, args = parse_args() - args.func(parser, args) - -main() diff --git a/discord.py-rewrite/discord/abc.py b/discord.py-rewrite/discord/abc.py deleted file mode 100644 index e836dda..0000000 --- a/discord.py-rewrite/discord/abc.py +++ /dev/null @@ -1,995 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import abc -import copy -import asyncio - -from collections import namedtuple - -from .iterators import HistoryIterator -from .context_managers import Typing -from .errors import InvalidArgument, ClientException -from .permissions import PermissionOverwrite, Permissions -from .role import Role -from .invite import Invite -from .file import File -from .voice_client import VoiceClient -from . import utils, compat - -class _Undefined: - def __repr__(self): - return 'see-below' - -_undefined = _Undefined() - -class Snowflake(metaclass=abc.ABCMeta): - """An ABC that details the common operations on a Discord model. - - Almost all :ref:`Discord models ` meet this - abstract base class. - - Attributes - ----------- - id: int - The model's unique ID. - """ - __slots__ = () - - @property - @abc.abstractmethod - def created_at(self): - """Returns the model's creation time in UTC.""" - raise NotImplementedError - - @classmethod - def __subclasshook__(cls, C): - if cls is Snowflake: - mro = C.__mro__ - for attr in ('created_at', 'id'): - for base in mro: - if attr in base.__dict__: - break - else: - return NotImplemented - return True - return NotImplemented - -class User(metaclass=abc.ABCMeta): - """An ABC that details the common operations on a Discord user. - - The following implement this ABC: - - - :class:`User` - - :class:`ClientUser` - - :class:`Member` - - This ABC must also implement :class:`abc.Snowflake`. - - Attributes - ----------- - name: str - The user's username. - discriminator: str - The user's discriminator. - avatar: Optional[str] - The avatar hash the user has. - bot: bool - If the user is a bot account. - """ - __slots__ = () - - @property - @abc.abstractmethod - def display_name(self): - """Returns the user's display name.""" - raise NotImplementedError - - @property - @abc.abstractmethod - def mention(self): - """Returns a string that allows you to mention the given user.""" - raise NotImplementedError - - @classmethod - def __subclasshook__(cls, C): - if cls is User: - if Snowflake.__subclasshook__(C) is NotImplemented: - return NotImplemented - - mro = C.__mro__ - for attr in ('display_name', 'mention', 'name', 'avatar', 'discriminator', 'bot'): - for base in mro: - if attr in base.__dict__: - break - else: - return NotImplemented - return True - return NotImplemented - -class PrivateChannel(metaclass=abc.ABCMeta): - """An ABC that details the common operations on a private Discord channel. - - The following implement this ABC: - - - :class:`DMChannel` - - :class:`GroupChannel` - - This ABC must also implement :class:`abc.Snowflake`. - - Attributes - ----------- - me: :class:`ClientUser` - The user presenting yourself. - """ - __slots__ = () - - @classmethod - def __subclasshook__(cls, C): - if cls is PrivateChannel: - if Snowflake.__subclasshook__(C) is NotImplemented: - return NotImplemented - - mro = C.__mro__ - for base in mro: - if 'me' in base.__dict__: - return True - return NotImplemented - return NotImplemented - -_Overwrites = namedtuple('_Overwrites', 'id allow deny type') - -class GuildChannel: - """An ABC that details the common operations on a Discord guild channel. - - The following implement this ABC: - - - :class:`TextChannel` - - :class:`VoiceChannel` - - :class:`CategoryChannel` - - This ABC must also implement :class:`abc.Snowflake`. - - Attributes - ----------- - name: str - The channel name. - guild: :class:`Guild` - The guild the channel belongs to. - position: int - The position in the channel list. This is a number that starts at 0. - e.g. the top channel is position 0. - """ - __slots__ = () - - def __str__(self): - return self.name - - @asyncio.coroutine - def _move(self, position, parent_id=None, lock_permissions=False, *, reason): - if position < 0: - raise InvalidArgument('Channel position cannot be less than 0.') - - http = self._state.http - cls = type(self) - channels = [c for c in self.guild.channels if isinstance(c, cls)] - - if position >= len(channels): - raise InvalidArgument('Channel position cannot be greater than {}'.format(len(channels) - 1)) - - channels.sort(key=lambda c: c.position) - - try: - # remove ourselves from the channel list - channels.remove(self) - except ValueError: - # not there somehow lol - return - else: - # add ourselves at our designated position - channels.insert(position, self) - - payload = [] - for index, c in enumerate(channels): - d = {'id': c.id, 'position': index} - if parent_id is not _undefined and c.id == self.id: - d.update(parent_id=parent_id, lock_permissions=lock_permissions) - payload.append(d) - - yield from http.bulk_channel_update(self.guild.id, payload, reason=reason) - self.position = position - if parent_id is not _undefined: - self.category_id = int(parent_id) if parent_id else None - - @asyncio.coroutine - def _edit(self, options, reason): - try: - parent = options.pop('category') - except KeyError: - parent_id = _undefined - else: - parent_id = parent and parent.id - - lock_permissions = options.pop('sync_permissions', False) - - try: - position = options.pop('position') - except KeyError: - if parent_id is not _undefined: - yield from self._move(self.position, parent_id=parent_id, lock_permissions=lock_permissions, reason=reason) - elif lock_permissions and self.category_id is not None: - # if we're syncing permissions on a pre-existing channel category without changing it - # we need to update the permissions to point to the pre-existing category - category = self.guild.get_channel(self.category_id) - options['permission_overwrites'] = [c._asdict() for c in category._overwrites] - else: - yield from self._move(position, parent_id=parent_id, lock_permissions=lock_permissions, reason=reason) - - if options: - data = yield from self._state.http.edit_channel(self.id, reason=reason, **options) - self._update(self.guild, data) - - def _fill_overwrites(self, data): - self._overwrites = [] - everyone_index = 0 - everyone_id = self.guild.id - - for index, overridden in enumerate(data.get('permission_overwrites', [])): - overridden_id = int(overridden.pop('id')) - self._overwrites.append(_Overwrites(id=overridden_id, **overridden)) - - if overridden['type'] == 'member': - continue - - if overridden_id == everyone_id: - # the @everyone role is not guaranteed to be the first one - # in the list of permission overwrites, however the permission - # resolution code kind of requires that it is the first one in - # the list since it is special. So we need the index so we can - # swap it to be the first one. - everyone_index = index - - # do the swap - tmp = self._overwrites - if tmp: - tmp[everyone_index], tmp[0] = tmp[0], tmp[everyone_index] - - @property - def changed_roles(self): - """Returns a list of :class:`Roles` that have been overridden from - their default values in the :attr:`Guild.roles` attribute.""" - ret = [] - for overwrite in filter(lambda o: o.type == 'role', self._overwrites): - role = utils.get(self.guild.roles, id=overwrite.id) - if role is None: - continue - - role = copy.copy(role) - role.permissions.handle_overwrite(overwrite.allow, overwrite.deny) - ret.append(role) - return ret - - @property - def mention(self): - """str : The string that allows you to mention the channel.""" - return '<#%s>' % self.id - - @property - def created_at(self): - """Returns the channel's creation time in UTC.""" - return utils.snowflake_time(self.id) - - def overwrites_for(self, obj): - """Returns the channel-specific overwrites for a member or a role. - - Parameters - ----------- - obj - The :class:`Role` or :class:`abc.User` denoting - whose overwrite to get. - - Returns - --------- - :class:`PermissionOverwrite` - The permission overwrites for this object. - """ - - if isinstance(obj, User): - predicate = lambda p: p.type == 'member' - elif isinstance(obj, Role): - predicate = lambda p: p.type == 'role' - else: - predicate = lambda p: True - - for overwrite in filter(predicate, self._overwrites): - if overwrite.id == obj.id: - allow = Permissions(overwrite.allow) - deny = Permissions(overwrite.deny) - return PermissionOverwrite.from_pair(allow, deny) - - return PermissionOverwrite() - - @property - def overwrites(self): - """Returns all of the channel's overwrites. - - This is returned as a list of two-element tuples containing the target, - which can be either a :class:`Role` or a :class:`Member` and the overwrite - as the second element as a :class:`PermissionOverwrite`. - - Returns - -------- - List[Tuple[Union[:class:`Role`, :class:`Member`], :class:`PermissionOverwrite`]]: - The channel's permission overwrites. - """ - ret = [] - for ow in self._overwrites: - allow = Permissions(ow.allow) - deny = Permissions(ow.deny) - overwrite = PermissionOverwrite.from_pair(allow, deny) - - if ow.type == 'role': - # accidentally quadratic - target = utils.find(lambda r: r.id == ow.id, self.guild.roles) - elif ow.type == 'member': - target = self.guild.get_member(ow.id) - - ret.append((target, overwrite)) - return ret - - @property - def category(self): - """Optional[:class:`CategoryChannel`]: The category this channel belongs to. - - If there is no category then this is ``None``. - """ - return self.guild.get_channel(self.category_id) - - def permissions_for(self, member): - """Handles permission resolution for the current :class:`Member`. - - This function takes into consideration the following cases: - - - Guild owner - - Guild roles - - Channel overrides - - Member overrides - - Parameters - ---------- - member : :class:`Member` - The member to resolve permissions for. - - Returns - ------- - :class:`Permissions` - The resolved permissions for the member. - """ - - # The current cases can be explained as: - # Guild owner get all permissions -- no questions asked. Otherwise... - # The @everyone role gets the first application. - # After that, the applied roles that the user has in the channel - # (or otherwise) are then OR'd together. - # After the role permissions are resolved, the member permissions - # have to take into effect. - # After all that is done.. you have to do the following: - - # If manage permissions is True, then all permissions are set to True. - - # The operation first takes into consideration the denied - # and then the allowed. - - o = self.guild.owner - if o is not None and member.id == o.id: - return Permissions.all() - - default = self.guild.default_role - base = Permissions(default.permissions.value) - - # Apply guild roles that the member has. - for role in member.roles: - base.value |= role.permissions.value - - # Guild-wide Administrator -> True for everything - # Bypass all channel-specific overrides - if base.administrator: - return Permissions.all() - - # Apply @everyone allow/deny first since it's special - try: - maybe_everyone = self._overwrites[0] - if maybe_everyone.id == self.guild.id: - base.handle_overwrite(allow=maybe_everyone.allow, deny=maybe_everyone.deny) - remaining_overwrites = self._overwrites[1:] - else: - remaining_overwrites = self._overwrites - except IndexError: - remaining_overwrites = self._overwrites - - member_role_ids = set(map(lambda r: r.id, member.roles)) - denies = 0 - allows = 0 - - # Apply channel specific role permission overwrites - for overwrite in remaining_overwrites: - if overwrite.type == 'role' and overwrite.id in member_role_ids: - denies |= overwrite.deny - allows |= overwrite.allow - - base.handle_overwrite(allow=allows, deny=denies) - - # Apply member specific permission overwrites - for overwrite in remaining_overwrites: - if overwrite.type == 'member' and overwrite.id == member.id: - base.handle_overwrite(allow=overwrite.allow, deny=overwrite.deny) - break - - # if you can't send a message in a channel then you can't have certain - # permissions as well - if not base.send_messages: - base.send_tts_messages = False - base.mention_everyone = False - base.embed_links = False - base.attach_files = False - - # if you can't read a channel then you have no permissions there - if not base.read_messages: - denied = Permissions.all_channel() - base.value &= ~denied.value - - return base - - @asyncio.coroutine - def delete(self, *, reason=None): - """|coro| - - Deletes the channel. - - You must have Manage Channel permission to use this. - - Parameters - ----------- - reason: Optional[str] - The reason for deleting this channel. - Shows up on the audit log. - - Raises - ------- - Forbidden - You do not have proper permissions to delete the channel. - NotFound - The channel was not found or was already deleted. - HTTPException - Deleting the channel failed. - """ - yield from self._state.http.delete_channel(self.id, reason=reason) - - @asyncio.coroutine - def set_permissions(self, target, *, overwrite=_undefined, reason=None, **permissions): - """|coro| - - Sets the channel specific permission overwrites for a target in the - channel. - - The ``target`` parameter should either be a :class:`Member` or a - :class:`Role` that belongs to guild. - - The ``overwrite`` parameter, if given, must either be ``None`` or - :class:`PermissionOverwrite`. For convenience, you can pass in - keyword arguments denoting :class:`Permissions` attributes. If this is - done, then you cannot mix the keyword arguments with the ``overwrite`` - parameter. - - If the ``overwrite`` parameter is ``None``, then the permission - overwrites are deleted. - - You must have :attr:`Permissions.manage_roles` permission to use this. - - Examples - ---------- - - Setting allow and deny: :: - - await message.channel.set_permissions(message.author, read_messages=True, - send_messages=False) - - Deleting overwrites :: - - await channel.set_permissions(member, overwrite=None) - - Using :class:`PermissionOverwrite` :: - - overwrite = PermissionOverwrite() - overwrite.send_messages = False - overwrite.read_messages = True - await channel.set_permissions(member, overwrite=overwrite) - - Parameters - ----------- - target - The :class:`Member` or :class:`Role` to overwrite permissions for. - overwrite: :class:`PermissionOverwrite` - The permissions to allow and deny to the target. - \*\*permissions - A keyword argument list of permissions to set for ease of use. - Cannot be mixed with ``overwrite``. - reason: Optional[str] - The reason for doing this action. Shows up on the audit log. - - Raises - ------- - Forbidden - You do not have permissions to edit channel specific permissions. - HTTPException - Editing channel specific permissions failed. - InvalidArgument - The overwrite parameter invalid or the target type was not - :class:`Role` or :class:`Member`. - """ - - http = self._state.http - - if isinstance(target, User): - perm_type = 'member' - elif isinstance(target, Role): - perm_type = 'role' - else: - raise InvalidArgument('target parameter must be either Member or Role') - - if isinstance(overwrite, _Undefined): - if len(permissions) == 0: - raise InvalidArgument('No overwrite provided.') - try: - overwrite = PermissionOverwrite(**permissions) - except: - raise InvalidArgument('Invalid permissions given to keyword arguments.') - else: - if len(permissions) > 0: - raise InvalidArgument('Cannot mix overwrite and keyword arguments.') - - # TODO: wait for event - - if overwrite is None: - yield from http.delete_channel_permissions(self.id, target.id, reason=reason) - elif isinstance(overwrite, PermissionOverwrite): - (allow, deny) = overwrite.pair() - yield from http.edit_channel_permissions(self.id, target.id, allow.value, deny.value, perm_type, reason=reason) - else: - raise InvalidArgument('Invalid overwrite type provided.') - - @asyncio.coroutine - def create_invite(self, *, reason=None, **fields): - """|coro| - - Creates an instant invite. - - Parameters - ------------ - max_age : int - How long the invite should last. If it's 0 then the invite - doesn't expire. Defaults to 0. - max_uses : int - How many uses the invite could be used for. If it's 0 then there - are unlimited uses. Defaults to 0. - temporary : bool - Denotes that the invite grants temporary membership - (i.e. they get kicked after they disconnect). Defaults to False. - unique: bool - Indicates if a unique invite URL should be created. Defaults to True. - If this is set to False then it will return a previously created - invite. - reason: Optional[str] - The reason for creating this invite. Shows up on the audit log. - - Raises - ------- - HTTPException - Invite creation failed. - - Returns - -------- - :class:`Invite` - The invite that was created. - """ - - data = yield from self._state.http.create_invite(self.id, reason=reason, **fields) - return Invite.from_incomplete(data=data, state=self._state) - - @asyncio.coroutine - def invites(self): - """|coro| - - Returns a list of all active instant invites from this channel. - - You must have proper permissions to get this information. - - Raises - ------- - Forbidden - You do not have proper permissions to get the information. - HTTPException - An error occurred while fetching the information. - - Returns - ------- - List[:class:`Invite`] - The list of invites that are currently active. - """ - - state = self._state - data = yield from state.http.invites_from_channel(self.id) - result = [] - - for invite in data: - invite['channel'] = self - invite['guild'] = self.guild - result.append(Invite(state=state, data=invite)) - - return result - -class Messageable(metaclass=abc.ABCMeta): - """An ABC that details the common operations on a model that can send messages. - - The following implement this ABC: - - - :class:`TextChannel` - - :class:`DMChannel` - - :class:`GroupChannel` - - :class:`User` - - :class:`Member` - - :class:`~ext.commands.Context` - - This ABC must also implement :class:`abc.Snowflake`. - """ - - __slots__ = () - - @asyncio.coroutine - @abc.abstractmethod - def _get_channel(self): - raise NotImplementedError - - @asyncio.coroutine - def send(self, content=None, *, tts=False, embed=None, file=None, files=None, delete_after=None, nonce=None): - """|coro| - - Sends a message to the destination with the content given. - - The content must be a type that can convert to a string through ``str(content)``. - If the content is set to ``None`` (the default), then the ``embed`` parameter must - be provided. - - To upload a single file, the ``file`` parameter should be used with a - single :class:`File` object. To upload multiple files, the ``files`` - parameter should be used with a list of :class:`File` objects. - **Specifying both parameters will lead to an exception**. - - If the ``embed`` parameter is provided, it must be of type :class:`Embed` and - it must be a rich embed type. - - Parameters - ------------ - content - The content of the message to send. - tts: bool - Indicates if the message should be sent using text-to-speech. - embed: :class:`Embed` - The rich embed for the content. - file: :class:`File` - The file to upload. - files: List[:class:`File`] - A list of files to upload. Must be a maximum of 10. - nonce: int - The nonce to use for sending this message. If the message was successfully sent, - then the message will have a nonce with this value. - delete_after: float - If provided, the number of seconds to wait in the background - before deleting the message we just sent. If the deletion fails, - then it is silently ignored. - - Raises - -------- - HTTPException - Sending the message failed. - Forbidden - You do not have the proper permissions to send the message. - InvalidArgument - The ``files`` list is not of the appropriate size or - you specified both ``file`` and ``files``. - - Returns - --------- - :class:`Message` - The message that was sent. - """ - - channel = yield from self._get_channel() - state = self._state - content = str(content) if content is not None else None - if embed is not None: - embed = embed.to_dict() - - if file is not None and files is not None: - raise InvalidArgument('cannot pass both file and files parameter to send()') - - if file is not None: - if not isinstance(file, File): - raise InvalidArgument('file parameter must be File') - - try: - data = yield from state.http.send_files(channel.id, files=[(file.open_file(), file.filename)], - content=content, tts=tts, embed=embed, nonce=nonce) - finally: - file.close() - - elif files is not None: - if len(files) > 10: - raise InvalidArgument('files parameter must be a list of up to 10 elements') - - try: - param = [(f.open_file(), f.filename) for f in files] - data = yield from state.http.send_files(channel.id, files=param, content=content, tts=tts, - embed=embed, nonce=nonce) - finally: - for f in files: - f.close() - else: - data = yield from state.http.send_message(channel.id, content, tts=tts, embed=embed, nonce=nonce) - - ret = state.create_message(channel=channel, data=data) - if delete_after is not None: - @asyncio.coroutine - def delete(): - yield from asyncio.sleep(delete_after, loop=state.loop) - try: - yield from ret.delete() - except: - pass - compat.create_task(delete(), loop=state.loop) - return ret - - @asyncio.coroutine - def trigger_typing(self): - """|coro| - - Triggers a *typing* indicator to the destination. - - *Typing* indicator will go away after 10 seconds, or after a message is sent. - """ - - channel = yield from self._get_channel() - yield from self._state.http.send_typing(channel.id) - - def typing(self): - """Returns a context manager that allows you to type for an indefinite period of time. - - This is useful for denoting long computations in your bot. - - .. note:: - - This is both a regular context manager and an async context manager. - This means that both ``with`` and ``async with`` work with this. - - Example Usage: :: - - async with channel.typing(): - # do expensive stuff here - await channel.send('done!') - - """ - return Typing(self) - - @asyncio.coroutine - def get_message(self, id): - """|coro| - - Retrieves a single :class:`Message` from the destination. - - This can only be used by bot accounts. - - Parameters - ------------ - id: int - The message ID to look for. - - Returns - -------- - :class:`Message` - The message asked for. - - Raises - -------- - NotFound - The specified message was not found. - Forbidden - You do not have the permissions required to get a message. - HTTPException - Retrieving the message failed. - """ - - channel = yield from self._get_channel() - data = yield from self._state.http.get_message(channel.id, id) - return self._state.create_message(channel=channel, data=data) - - @asyncio.coroutine - def pins(self): - """|coro| - - Returns a list of :class:`Message` that are currently pinned. - - Raises - ------- - HTTPException - Retrieving the pinned messages failed. - """ - - channel = yield from self._get_channel() - state = self._state - data = yield from state.http.pins_from(channel.id) - return [state.create_message(channel=channel, data=m) for m in data] - - def history(self, *, limit=100, before=None, after=None, around=None, reverse=None): - """Return an :class:`AsyncIterator` that enables receiving the destination's message history. - - You must have :attr:`~Permissions.read_message_history` permissions to use this. - - All parameters are optional. - - Parameters - ----------- - limit: Optional[int] - The number of messages to retrieve. - If ``None``, retrieves every message in the channel. Note, however, - that this would make it a slow operation. - before: :class:`Message` or `datetime` - Retrieve messages before this date or message. - If a date is provided it must be a timezone-naive datetime representing UTC time. - after: :class:`Message` or `datetime` - Retrieve messages after this date or message. - If a date is provided it must be a timezone-naive datetime representing UTC time. - around: :class:`Message` or `datetime` - Retrieve messages around this date or message. - If a date is provided it must be a timezone-naive datetime representing UTC time. - When using this argument, the maximum limit is 101. Note that if the limit is an - even number then this will return at most limit + 1 messages. - reverse: bool - If set to true, return messages in oldest->newest order. If unspecified, - this defaults to ``False`` for most cases. However if passing in a - ``after`` parameter then this is set to ``True``. This avoids getting messages - out of order in the ``after`` case. - - Raises - ------ - Forbidden - You do not have permissions to get channel message history. - HTTPException - The request to get message history failed. - - Yields - ------- - :class:`Message` - The message with the message data parsed. - - Examples - --------- - - Usage :: - - counter = 0 - async for message in channel.history(limit=200): - if message.author == client.user: - counter += 1 - - Flattening into a list: :: - - messages = await channel.history(limit=123).flatten() - # messages is now a list of Message... - - Python 3.4 Usage :: - - count = 0 - iterator = channel.history(limit=200) - while True: - try: - message = yield from iterator.next() - except discord.NoMoreItems: - break - else: - if message.author == client.user: - counter += 1 - """ - return HistoryIterator(self, limit=limit, before=before, after=after, around=around, reverse=reverse) - - -class Connectable(metaclass=abc.ABCMeta): - """An ABC that details the common operations on a channel that can - connect to a voice server. - - The following implement this ABC: - - - :class:`VoiceChannel` - """ - __slots__ = () - - @abc.abstractmethod - def _get_voice_client_key(self): - raise NotImplementedError - - @abc.abstractmethod - def _get_voice_state_pair(self): - raise NotImplementedError - - @asyncio.coroutine - def connect(self, *, timeout=60.0, reconnect=True): - """|coro| - - Connects to voice and creates a :class:`VoiceClient` to establish - your connection to the voice server. - - Parameters - ----------- - timeout: float - The timeout in seconds to wait for the voice endpoint. - reconnect: bool - Whether the bot should automatically attempt - a reconnect if a part of the handshake fails - or the gateway goes down. - - Raises - ------- - asyncio.TimeoutError - Could not connect to the voice channel in time. - ClientException - You are already connected to a voice channel. - OpusNotLoaded - The opus library has not been loaded. - - Returns - ------- - :class:`VoiceClient` - A voice client that is fully connected to the voice server. - """ - key_id, key_name = self._get_voice_client_key() - state = self._state - - if state._get_voice_client(key_id): - raise ClientException('Already connected to a voice channel.') - - voice = VoiceClient(state=state, timeout=timeout, channel=self) - state._add_voice_client(key_id, voice) - - try: - yield from voice.connect(reconnect=reconnect) - except asyncio.TimeoutError as e: - try: - yield from voice.disconnect(force=True) - except: - # we don't care if disconnect failed because connection failed - pass - raise e # re-raise - - return voice diff --git a/discord.py-rewrite/discord/audit_logs.py b/discord.py-rewrite/discord/audit_logs.py deleted file mode 100644 index c33f8c3..0000000 --- a/discord.py-rewrite/discord/audit_logs.py +++ /dev/null @@ -1,340 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from . import utils, enums -from .object import Object -from .permissions import PermissionOverwrite, Permissions -from .colour import Colour -from .invite import Invite - -def _transform_verification_level(entry, data): - return enums.try_enum(enums.VerificationLevel, data) - -def _transform_explicit_content_filter(entry, data): - return enums.try_enum(enums.ContentFilter, data) - -def _transform_permissions(entry, data): - return Permissions(data) - -def _transform_color(entry, data): - return Colour(data) - -def _transform_snowflake(entry, data): - return int(data) - -def _transform_channel(entry, data): - if data is None: - return None - channel = entry.guild.get_channel(int(data)) or Object(id=data) - return channel - -def _transform_owner_id(entry, data): - if data is None: - return None - return entry._get_member(int(data)) - -def _transform_inviter_id(entry, data): - if data is None: - return None - return entry._get_member(int(data)) - -def _transform_overwrites(entry, data): - overwrites = [] - for elem in data: - allow = Permissions(elem['allow']) - deny = Permissions(elem['deny']) - ow = PermissionOverwrite.from_pair(allow, deny) - - ow_type = elem['type'] - ow_id = int(elem['id']) - if ow_type == 'role': - target = utils.find(lambda r: r.id == ow_id, entry.guild.roles) - else: - target = entry._get_member(ow_id) - - if target is None: - target = Object(id=ow_id) - - overwrites.append((target, ow)) - - return overwrites - -class AuditLogDiff: - def __len__(self): - return len(self.__dict__) - - def __iter__(self): - return iter(self.__dict__.items()) - - def __repr__(self): - return ''.format(tuple(self.__dict__)) - -class AuditLogChanges: - TRANSFORMERS = { - 'verification_level': (None, _transform_verification_level), - 'explicit_content_filter': (None, _transform_explicit_content_filter), - 'allow': (None, _transform_permissions), - 'deny': (None, _transform_permissions), - 'permissions': (None, _transform_permissions), - 'id': (None, _transform_snowflake), - 'color': ('colour', _transform_color), - 'owner_id': ('owner', _transform_owner_id), - 'inviter_id': ('inviter', _transform_inviter_id), - 'channel_id': ('channel', _transform_channel), - 'afk_channel_id': ('afk_channel', _transform_channel), - 'system_channel_id': ('system_channel', _transform_channel), - 'widget_channel_id': ('widget_channel', _transform_channel), - 'permission_overwrites': ('overwrites', _transform_overwrites), - 'splash_hash': ('splash', None), - 'icon_hash': ('icon', None), - 'avatar_hash': ('avatar', None), - } - - def __init__(self, entry, data): - self.before = AuditLogDiff() - self.after = AuditLogDiff() - - for elem in data: - attr = elem['key'] - - # special cases for role add/remove - if attr == '$add': - self._handle_role(self.before, self.after, entry, elem['new_value']) - continue - elif attr == '$remove': - self._handle_role(self.after, self.before, entry, elem['new_value']) - continue - - transformer = self.TRANSFORMERS.get(attr) - if transformer: - key, transformer = transformer - if key: - attr = key - - try: - before = elem['old_value'] - except KeyError: - before = None - else: - if transformer: - before = transformer(entry, before) - - setattr(self.before, attr, before) - - try: - after = elem['new_value'] - except KeyError: - after = None - else: - if transformer: - after = transformer(entry, after) - - setattr(self.after, attr, after) - - # add an alias - if hasattr(self.after, 'colour'): - self.after.color = self.after.colour - self.before.color = self.before.colour - - def _handle_role(self, first, second, entry, elem): - setattr(first, 'roles', []) - - data = [] - roles = entry.guild.roles - - for e in elem: - role_id = int(e['id']) - role = utils.find(lambda r: r.id == role_id, roles) - - if role is None: - role = Object(id=role_id) - role.name = e['name'] - - data.append(role) - - setattr(second, 'roles', data) - -class AuditLogEntry: - """Represents an Audit Log entry. - - You retrieve these via :meth:`Guild.audit_logs`. - - Attributes - ----------- - action: :class:`AuditLogAction` - The action that was done. - user: :class:`abc.User` - The user who initiated this action. Usually a :class:`Member`\, unless gone - then it's a :class:`User`. - id: int - The entry ID. - target: Any - The target that got changed. The exact type of this depends on - the action being done. - reason: Optional[str] - The reason this action was done. - extra: Any - Extra information that this entry has that might be useful. - For most actions, this is ``None``. However in some cases it - contains extra information. See :class:`AuditLogAction` for - which actions have this field filled out. - """ - - def __init__(self, *, users, data, guild): - self._state = guild._state - self.guild = guild - self._users = users - self._from_data(data) - - def _from_data(self, data): - self.action = enums.AuditLogAction(data['action_type']) - self.id = int(data['id']) - - # this key is technically not usually present - self.reason = data.get('reason') - self.extra = data.get('options') - - if self.extra: - if self.action is enums.AuditLogAction.member_prune: - # member prune has two keys with useful information - self.extra = type('_AuditLogProxy', (), {k: int(v) for k, v in self.extra.items()})() - elif self.action is enums.AuditLogAction.message_delete: - channel_id = int(self.extra['channel_id']) - elems = { - 'count': int(self.extra['count']), - 'channel': self.guild.get_channel(channel_id) or Object(id=channel_id) - } - self.extra = type('_AuditLogProxy', (), elems)() - elif self.action.name.startswith('overwrite_'): - # the overwrite_ actions have a dict with some information - instance_id = int(self.extra['id']) - the_type = self.extra.get('type') - if the_type == 'member': - self.extra = self._get_member(instance_id) - else: - role = utils.find(lambda r: r.id == instance_id, self.guild.roles) - if role is None: - role = Object(id=instance_id) - role.name = self.extra.get('role_name') - self.extra = role - - # this key is not present when the above is present, typically. - # It's a list of { new_value: a, old_value: b, key: c } - # where new_value and old_value are not guaranteed to be there depending - # on the action type, so let's just fetch it for now and only turn it - # into meaningful data when requested - self._changes = data.get('changes', []) - - self.user = self._get_member(utils._get_as_snowflake(data, 'user_id')) - self._target_id = utils._get_as_snowflake(data, 'target_id') - - def _get_member(self, user_id): - return self.guild.get_member(user_id) or self._users.get(user_id) - - def __repr__(self): - return ''.format(self) - - @utils.cached_property - def created_at(self): - """Returns the entry's creation time in UTC.""" - return utils.snowflake_time(self.id) - - @utils.cached_property - def target(self): - try: - converter = getattr(self, '_convert_target_' + self.action.target_type) - except AttributeError: - return Object(id=self._target_id) - else: - return converter(self._target_id) - - @utils.cached_property - def category(self): - """Optional[:class:`AuditLogActionCategory`]: The category of the action, if applicable.""" - return self.action.category - - @utils.cached_property - def changes(self): - """:class:`AuditLogChanges`: The list of changes this entry has.""" - obj = AuditLogChanges(self, self._changes) - del self._changes - return obj - - @utils.cached_property - def before(self): - """:class:`AuditLogDiff`: The target's prior state.""" - return self.changes.before - - @utils.cached_property - def after(self): - """:class:`AuditLogDiff`: The target's subsequent state.""" - return self.changes.after - - def _convert_target_guild(self, target_id): - return self.guild - - def _convert_target_channel(self, target_id): - ch = self.guild.get_channel(target_id) - if ch is None: - return Object(id=target_id) - return ch - - def _convert_target_user(self, target_id): - return self._get_member(target_id) - - def _convert_target_role(self, target_id): - role = utils.find(lambda r: r.id == target_id, self.guild.roles) - if role is None: - return Object(id=target_id) - return role - - def _convert_target_invite(self, target_id): - # invites have target_id set to null - # so figure out which change has the full invite data - changeset = self.before if self.action is enums.AuditLogAction.invite_delete else self.after - - fake_payload = { - 'max_age': changeset.max_age, - 'max_uses': changeset.max_uses, - 'code': changeset.code, - 'temporary': changeset.temporary, - 'channel': changeset.channel, - 'uses': changeset.uses, - 'guild': self.guild, - } - - obj = Invite(state=self._state, data=fake_payload) - try: - obj.inviter = changeset.inviter - except AttributeError: - pass - return obj - - def _convert_target_emoji(self, target_id): - return self._state.get_emoji(target_id) or Object(id=target_id) - - def _convert_target_message(self, target_id): - return self._get_member(target_id) diff --git a/discord.py-rewrite/discord/backoff.py b/discord.py-rewrite/discord/backoff.py deleted file mode 100644 index 3fa52d9..0000000 --- a/discord.py-rewrite/discord/backoff.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import time -import random - -class ExponentialBackoff: - """An implementation of the exponential backoff algorithm - - Provides a convenient interface to implement an exponential backoff - for reconnecting or retrying transmissions in a distributed network. - - Once instantiated, the delay method will return the next interval to - wait for when retrying a connection or transmission. The maximum - delay increases exponentially with each retry up to a maximum of - 2^10 * base, and is reset if no more attempts are needed in a period - of 2^11 * base seconds. - - Parameters - ---------- - base: int - The base delay in seconds. The first retry-delay will be up to - this many seconds. - integral: bool - Set to True if whole periods of base is desirable, otherwise any - number in between may be returned. - """ - - def __init__(self, base=1, *, integral=False): - self._base = base - - self._exp = 0 - self._max = 10 - self._reset_time = base * 2 ** 11 - self._last_invocation = time.monotonic() - - # Use our own random instance to avoid messing with global one - rand = random.Random() - rand.seed() - - self._randfunc = rand.randrange if integral else rand.uniform - - def delay(self): - """Compute the next delay - - Returns the next delay to wait according to the exponential - backoff algorithm. This is a value between 0 and base * 2^exp - where exponent starts off at 1 and is incremented at every - invocation of this method up to a maximum of 10. - - If a period of more than base * 2^11 has passed since the last - retry, the exponent is reset to 1. - """ - invocation = time.monotonic() - interval = invocation - self._last_invocation - self._last_invocation = invocation - - if interval > self._reset_time: - self._exp = 0 - - self._exp = min(self._exp + 1, self._max) - return self._randfunc(0, self._base * 2 ** self._exp) diff --git a/discord.py-rewrite/discord/calls.py b/discord.py-rewrite/discord/calls.py deleted file mode 100644 index 0ed5f23..0000000 --- a/discord.py-rewrite/discord/calls.py +++ /dev/null @@ -1,156 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import datetime - -from . import utils -from .enums import VoiceRegion, try_enum -from .member import VoiceState - -class CallMessage: - """Represents a group call message from Discord. - - This is only received in cases where the message type is equivalent to - :attr:`MessageType.call`. - - Attributes - ----------- - ended_timestamp: Optional[datetime.datetime] - A naive UTC datetime object that represents the time that the call has ended. - participants: List[:class:`User`] - The list of users that are participating in this call. - message: :class:`Message` - The message associated with this call message. - """ - - def __init__(self, message, **kwargs): - self.message = message - self.ended_timestamp = utils.parse_time(kwargs.get('ended_timestamp')) - self.participants = kwargs.get('participants') - - @property - def call_ended(self): - """bool: Indicates if the call has ended.""" - return self.ended_timestamp is not None - - @property - def channel(self): - """:class:`GroupChannel`\: The private channel associated with this message.""" - return self.message.channel - - @property - def duration(self): - """Queries the duration of the call. - - If the call has not ended then the current duration will - be returned. - - Returns - --------- - datetime.timedelta - The timedelta object representing the duration. - """ - if self.ended_timestamp is None: - return datetime.datetime.utcnow() - self.message.timestamp - else: - return self.ended_timestamp - self.message.timestamp - -class GroupCall: - """Represents the actual group call from Discord. - - This is accompanied with a :class:`CallMessage` denoting the information. - - Attributes - ----------- - call: :class:`CallMessage` - The call message associated with this group call. - unavailable: bool - Denotes if this group call is unavailable. - ringing: List[:class:`User`] - A list of users that are currently being rung to join the call. - region: :class:`VoiceRegion` - The guild region the group call is being hosted on. - """ - - def __init__(self, **kwargs): - self.call = kwargs.get('call') - self.unavailable = kwargs.get('unavailable') - self._voice_states = {} - - for state in kwargs.get('voice_states', []): - self._update_voice_state(state) - - self._update(**kwargs) - - def _update(self, **kwargs): - self.region = try_enum(VoiceRegion, kwargs.get('region')) - lookup = {u.id: u for u in self.call.channel.recipients} - me = self.call.channel.me - lookup[me.id] = me - self.ringing = list(filter(None, map(lambda i: lookup.get(i), kwargs.get('ringing', [])))) - - def _update_voice_state(self, data): - user_id = int(data['user_id']) - # left the voice channel? - if data['channel_id'] is None: - self._voice_states.pop(user_id, None) - else: - self._voice_states[user_id] = VoiceState(data=data, channel=self.channel) - - @property - def connected(self): - """A property that returns the list of :class:`User` that are currently in this call.""" - ret = [u for u in self.channel.recipients if self.voice_state_for(u) is not None] - me = self.channel.me - if self.voice_state_for(me) is not None: - ret.append(me) - - return ret - - @property - def channel(self): - """:class:`GroupChannel`\: Returns the channel the group call is in.""" - return self.call.channel - - def voice_state_for(self, user): - """Retrieves the :class:`VoiceState` for a specified :class:`User`. - - If the :class:`User` has no voice state then this function returns - ``None``. - - Parameters - ------------ - user: :class:`User` - The user to retrieve the voice state for. - - Returns - -------- - Optional[:class:`VoiceState`] - The voice state associated with this user. - """ - - return self._voice_states.get(user.id) - diff --git a/discord.py-rewrite/discord/channel.py b/discord.py-rewrite/discord/channel.py deleted file mode 100644 index 363db25..0000000 --- a/discord.py-rewrite/discord/channel.py +++ /dev/null @@ -1,930 +0,0 @@ -# -*- coding: utf-8 -*- -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from .permissions import Permissions -from .enums import ChannelType, try_enum -from .mixins import Hashable -from . import utils -from .errors import ClientException, NoMoreItems -from .webhook import Webhook - -import discord.abc - -import time -import asyncio - -__all__ = ('TextChannel', 'VoiceChannel', 'DMChannel', 'CategoryChannel', 'GroupChannel', '_channel_factory') - -@asyncio.coroutine -def _single_delete_strategy(messages): - for m in messages: - yield from m.delete() - -class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): - """Represents a Discord guild text channel. - - .. container:: operations - - .. describe:: x == y - - Checks if two channels are equal. - - .. describe:: x != y - - Checks if two channels are not equal. - - .. describe:: hash(x) - - Returns the channel's hash. - - .. describe:: str(x) - - Returns the channel's name. - - Attributes - ----------- - name: str - The channel name. - guild: :class:`Guild` - The guild the channel belongs to. - id: int - The channel ID. - category_id: int - The category channel ID this channel belongs to. - topic: Optional[str] - The channel's topic. None if it doesn't exist. - position: int - The position in the channel list. This is a number that starts at 0. e.g. the - top channel is position 0. - """ - - __slots__ = ( 'name', 'id', 'guild', 'topic', '_state', 'nsfw', - 'category_id', 'position', '_overwrites' ) - - def __init__(self, *, state, guild, data): - self._state = state - self.id = int(data['id']) - self._update(guild, data) - - def __repr__(self): - return ''.format(self) - - def _update(self, guild, data): - self.guild = guild - self.name = data['name'] - self.category_id = utils._get_as_snowflake(data, 'parent_id') - self.topic = data.get('topic') - self.position = data['position'] - self.nsfw = data.get('nsfw', False) - self._fill_overwrites(data) - - @asyncio.coroutine - def _get_channel(self): - return self - - def permissions_for(self, member): - base = super().permissions_for(member) - - # text channels do not have voice related permissions - denied = Permissions.voice() - base.value &= ~denied.value - return base - - permissions_for.__doc__ = discord.abc.GuildChannel.permissions_for.__doc__ - - @property - def members(self): - """Returns a list of :class:`Member` that can see this channel.""" - return [m for m in self.guild.members if self.permissions_for(m).read_messages] - - def is_nsfw(self): - """Checks if the channel is NSFW.""" - n = self.name - return self.nsfw or n == 'nsfw' or n[:5] == 'nsfw-' - - @asyncio.coroutine - def edit(self, *, reason=None, **options): - """|coro| - - Edits the channel. - - You must have the :attr:`Permissions.manage_channel` permission to - use this. - - Parameters - ---------- - name: str - The new channel name. - topic: str - The new channel's topic. - position: int - The new channel's position. - nsfw: bool - To mark the channel as NSFW or not. - sync_permissions: bool - Whether to sync permissions with the channel's new or pre-existing - category. Defaults to ``False``. - category: Optional[:class:`CategoryChannel`] - The new category for this channel. Can be ``None`` to remove the - category. - reason: Optional[str] - The reason for editing this channel. Shows up on the audit log. - - Raises - ------ - InvalidArgument - If position is less than 0 or greater than the number of channels. - Forbidden - You do not have permissions to edit the channel. - HTTPException - Editing the channel failed. - """ - yield from self._edit(options, reason=reason) - - @asyncio.coroutine - def delete_messages(self, messages): - """|coro| - - Deletes a list of messages. This is similar to :meth:`Message.delete` - except it bulk deletes multiple messages. - - As a special case, if the number of messages is 0, then nothing - is done. If the number of messages is 1 then single message - delete is done. If it's more than two, then bulk delete is used. - - You cannot bulk delete more than 100 messages or messages that - are older than 14 days old. - - Usable only by bot accounts. - - Parameters - ----------- - messages: Iterable[:class:`abc.Snowflake`] - An iterable of messages denoting which ones to bulk delete. - - Raises - ------ - ClientException - The number of messages to delete was more than 100. - Forbidden - You do not have proper permissions to delete the messages or - you're not using a bot account. - HTTPException - Deleting the messages failed. - """ - if not isinstance(messages, (list, tuple)): - messages = list(messages) - - if len(messages) == 0: - return # do nothing - - if len(messages) == 1: - message_id = messages[0].id - yield from self._state.http.delete_message(self.id, message_id) - return - - if len(messages) > 100: - raise ClientException('Can only bulk delete messages up to 100 messages') - - message_ids = [m.id for m in messages] - yield from self._state.http.delete_messages(self.id, message_ids) - - @asyncio.coroutine - def purge(self, *, limit=100, check=None, before=None, after=None, around=None, reverse=False, bulk=True): - """|coro| - - Purges a list of messages that meet the criteria given by the predicate - ``check``. If a ``check`` is not provided then all messages are deleted - without discrimination. - - You must have :attr:`Permissions.manage_messages` permission to delete - messages even if they are your own (unless you are a user account). - The :attr:`Permissions.read_message_history` permission is also needed - to retrieve message history. - - Internally, this employs a different number of strategies depending - on the conditions met such as if a bulk delete is possible or if - the account is a user bot or not. - - Parameters - ----------- - limit: int - The number of messages to search through. This is not the number - of messages that will be deleted, though it can be. - check: predicate - The function used to check if a message should be deleted. - It must take a :class:`Message` as its sole parameter. - before - Same as ``before`` in :meth:`history`. - after - Same as ``after`` in :meth:`history`. - around - Same as ``around`` in :meth:`history`. - reverse - Same as ``reverse`` in :meth:`history`. - bulk: bool - If True, use bulk delete. bulk=False is useful for mass-deleting - a bot's own messages without manage_messages. When True, will fall - back to single delete if current account is a user bot, or if - messages are older than two weeks. - - Raises - ------- - Forbidden - You do not have proper permissions to do the actions required. - HTTPException - Purging the messages failed. - - Examples - --------- - - Deleting bot's messages :: - - def is_me(m): - return m.author == client.user - - deleted = await channel.purge(limit=100, check=is_me) - await channel.send('Deleted {} message(s)'.format(len(deleted))) - - Returns - -------- - list - The list of messages that were deleted. - """ - - if check is None: - check = lambda m: True - - iterator = self.history(limit=limit, before=before, after=after, reverse=reverse, around=around) - ret = [] - count = 0 - - minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22 - strategy = self.delete_messages if self._state.is_bot and bulk else _single_delete_strategy - - while True: - try: - msg = yield from iterator.next() - except NoMoreItems: - # no more messages to poll - if count >= 2: - # more than 2 messages -> bulk delete - to_delete = ret[-count:] - yield from strategy(to_delete) - elif count == 1: - # delete a single message - yield from ret[-1].delete() - - return ret - else: - if count == 100: - # we've reached a full 'queue' - to_delete = ret[-100:] - yield from strategy(to_delete) - count = 0 - yield from asyncio.sleep(1) - - if check(msg): - if msg.id < minimum_time: - # older than 14 days old - if count == 1: - yield from ret[-1].delete() - elif count >= 2: - to_delete = ret[-count:] - yield from strategy(to_delete) - - count = 0 - strategy = _single_delete_strategy - - count += 1 - ret.append(msg) - - @asyncio.coroutine - def webhooks(self): - """|coro| - - Gets the list of webhooks from this channel. - - Requires :attr:`~.Permissions.manage_webhooks` permissions. - - Raises - ------- - Forbidden - You don't have permissions to get the webhooks. - - Returns - -------- - List[:class:`Webhook`] - The webhooks for this channel. - """ - - data = yield from self._state.http.channel_webhooks(self.id) - return [Webhook.from_state(d, state=self._state) for d in data] - - @asyncio.coroutine - def create_webhook(self, *, name=None, avatar=None): - """|coro| - - Creates a webhook for this channel. - - Requires :attr:`~.Permissions.manage_webhooks` permissions. - - Parameters - ------------- - name: Optional[str] - The webhook's name. - avatar: Optional[bytes] - A *bytes-like* object representing the webhook's default avatar. - This operates similarly to :meth:`~ClientUser.edit`. - - Raises - ------- - HTTPException - Creating the webhook failed. - Forbidden - You do not have permissions to create a webhook. - - Returns - -------- - :class:`Webhook` - The created webhook. - """ - - if avatar is not None: - avatar = utils._bytes_to_base64_data(avatar) - - if name is not None: - name = str(name) - - data = yield from self._state.http.create_webhook(self.id, name=name, avatar=avatar) - return Webhook.from_state(data, state=self._state) - -class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable): - """Represents a Discord guild voice channel. - - .. container:: operations - - .. describe:: x == y - - Checks if two channels are equal. - - .. describe:: x != y - - Checks if two channels are not equal. - - .. describe:: hash(x) - - Returns the channel's hash. - - .. describe:: str(x) - - Returns the channel's name. - - Attributes - ----------- - name: str - The channel name. - guild: :class:`Guild` - The guild the channel belongs to. - id: int - The channel ID. - category_id: int - The category channel ID this channel belongs to. - position: int - The position in the channel list. This is a number that starts at 0. e.g. the - top channel is position 0. - bitrate: int - The channel's preferred audio bitrate in bits per second. - user_limit: int - The channel's limit for number of members that can be in a voice channel. - """ - - __slots__ = ('name', 'id', 'guild', 'bitrate', 'user_limit', - '_state', 'position', '_overwrites', 'category_id' ) - - def __init__(self, *, state, guild, data): - self._state = state - self.id = int(data['id']) - self._update(guild, data) - - def __repr__(self): - return ''.format(self) - - def _get_voice_client_key(self): - return self.guild.id, 'guild_id' - - def _get_voice_state_pair(self): - return self.guild.id, self.id - - def _update(self, guild, data): - self.guild = guild - self.name = data['name'] - self.category_id = utils._get_as_snowflake(data, 'parent_id') - self.position = data['position'] - self.bitrate = data.get('bitrate') - self.user_limit = data.get('user_limit') - self._fill_overwrites(data) - - @property - def members(self): - """Returns a list of :class:`Member` that are currently inside this voice channel.""" - ret = [] - for user_id, state in self.guild._voice_states.items(): - if state.channel.id == self.id: - member = self.guild.get_member(user_id) - if member is not None: - ret.append(member) - return ret - - @asyncio.coroutine - def edit(self, *, reason=None, **options): - """|coro| - - Edits the channel. - - You must have the :attr:`Permissions.manage_channel` permission to - use this. - - Parameters - ---------- - bitrate: int - The new channel's bitrate. - user_limit: int - The new channel's user limit. - position: int - The new channel's position. - sync_permissions: bool - Whether to sync permissions with the channel's new or pre-existing - category. Defaults to ``False``. - category: Optional[:class:`CategoryChannel`] - The new category for this channel. Can be ``None`` to remove the - category. - reason: Optional[str] - The reason for editing this channel. Shows up on the audit log. - - Raises - ------ - Forbidden - You do not have permissions to edit the channel. - HTTPException - Editing the channel failed. - """ - - yield from self._edit(options, reason=reason) - -class CategoryChannel(discord.abc.GuildChannel, Hashable): - """Represents a Discord channel category. - - These are useful to group channels to logical compartments. - - .. container:: operations - - .. describe:: x == y - - Checks if two channels are equal. - - .. describe:: x != y - - Checks if two channels are not equal. - - .. describe:: hash(x) - - Returns the category's hash. - - .. describe:: str(x) - - Returns the category's name. - - Attributes - ----------- - name: str - The category name. - guild: :class:`Guild` - The guild the category belongs to. - id: int - The category channel ID. - position: int - The position in the category list. This is a number that starts at 0. e.g. the - top category is position 0. - """ - - __slots__ = ('name', 'id', 'guild', 'nsfw', '_state', 'position', '_overwrites', 'category_id') - - def __init__(self, *, state, guild, data): - self._state = state - self.id = int(data['id']) - self._update(guild, data) - - def __repr__(self): - return ''.format(self) - - def _update(self, guild, data): - self.guild = guild - self.name = data['name'] - self.category_id = utils._get_as_snowflake(data, 'parent_id') - self.nsfw = data.get('nsfw', False) - self.position = data['position'] - self._fill_overwrites(data) - - def is_nsfw(self): - """Checks if the category is NSFW.""" - n = self.name - return self.nsfw or n == 'nsfw' or n[:5] == 'nsfw-' - - @asyncio.coroutine - def edit(self, *, reason=None, **options): - """|coro| - - Edits the channel. - - You must have the :attr:`Permissions.manage_channel` permission to - use this. - - Parameters - ---------- - name: str - The new category's name. - position: int - The new category's position. - nsfw: bool - To mark the category as NSFW or not. - reason: Optional[str] - The reason for editing this category. Shows up on the audit log. - - Raises - ------ - InvalidArgument - If position is less than 0 or greater than the number of categories. - Forbidden - You do not have permissions to edit the category. - HTTPException - Editing the category failed. - """ - - try: - position = options.pop('position') - except KeyError: - pass - else: - yield from self._move(position, reason=reason) - self.position = position - - if options: - data = yield from self._state.http.edit_channel(self.id, reason=reason, **options) - self._update(self.guild, data) - - @property - def channels(self): - """List[:class:`abc.GuildChannel`]: Returns the channels that are under this category. - - These are sorted by the official Discord UI, which places voice channels below the text channels. - """ - def comparator(channel): - return (not isinstance(channel, TextChannel), channel.position) - - ret = [c for c in self.guild.channels if c.category_id == self.id] - ret.sort(key=comparator) - return ret - -class DMChannel(discord.abc.Messageable, Hashable): - """Represents a Discord direct message channel. - - .. container:: operations - - .. describe:: x == y - - Checks if two channels are equal. - - .. describe:: x != y - - Checks if two channels are not equal. - - .. describe:: hash(x) - - Returns the channel's hash. - - .. describe:: str(x) - - Returns a string representation of the channel - - Attributes - ---------- - recipient: :class:`User` - The user you are participating with in the direct message channel. - me: :class:`ClientUser` - The user presenting yourself. - id: int - The direct message channel ID. - """ - - __slots__ = ('id', 'recipient', 'me', '_state') - - def __init__(self, *, me, state, data): - self._state = state - self.recipient = state.store_user(data['recipients'][0]) - self.me = me - self.id = int(data['id']) - - @asyncio.coroutine - def _get_channel(self): - return self - - def __str__(self): - return 'Direct Message with %s' % self.recipient - - def __repr__(self): - return ''.format(self) - - @property - def created_at(self): - """Returns the direct message channel's creation time in UTC.""" - return utils.snowflake_time(self.id) - - def permissions_for(self, user=None): - """Handles permission resolution for a :class:`User`. - - This function is there for compatibility with other channel types. - - Actual direct messages do not really have the concept of permissions. - - This returns all the Text related permissions set to true except: - - - send_tts_messages: You cannot send TTS messages in a DM. - - manage_messages: You cannot delete others messages in a DM. - - Parameters - ----------- - user: :class:`User` - The user to check permissions for. This parameter is ignored - but kept for compatibility. - - Returns - -------- - :class:`Permissions` - The resolved permissions. - """ - - base = Permissions.text() - base.send_tts_messages = False - base.manage_messages = False - return base - -class GroupChannel(discord.abc.Messageable, Hashable): - """Represents a Discord group channel. - - .. container:: operations - - .. describe:: x == y - - Checks if two channels are equal. - - .. describe:: x != y - - Checks if two channels are not equal. - - .. describe:: hash(x) - - Returns the channel's hash. - - .. describe:: str(x) - - Returns a string representation of the channel - - Attributes - ---------- - recipients: list of :class:`User` - The users you are participating with in the group channel. - me: :class:`ClientUser` - The user presenting yourself. - id: int - The group channel ID. - owner: :class:`User` - The user that owns the group channel. - icon: Optional[str] - The group channel's icon hash if provided. - name: Optional[str] - The group channel's name if provided. - """ - - __slots__ = ('id', 'recipients', 'owner', 'icon', 'name', 'me', '_state') - - def __init__(self, *, me, state, data): - self._state = state - self.id = int(data['id']) - self.me = me - self._update_group(data) - - def _update_group(self, data): - owner_id = utils._get_as_snowflake(data, 'owner_id') - self.icon = data.get('icon') - self.name = data.get('name') - - try: - self.recipients = [self._state.store_user(u) for u in data['recipients']] - except KeyError: - pass - - if owner_id == self.me.id: - self.owner = self.me - else: - self.owner = utils.find(lambda u: u.id == owner_id, self.recipients) - - @asyncio.coroutine - def _get_channel(self): - return self - - def __str__(self): - if self.name: - return self.name - - if len(self.recipients) == 0: - return 'Unnamed' - - return ', '.join(map(lambda x: x.name, self.recipients)) - - def __repr__(self): - return ''.format(self) - - @property - def icon_url(self): - """Returns the channel's icon URL if available or an empty string otherwise.""" - if self.icon is None: - return '' - - return 'https://cdn.discordapp.com/channel-icons/{0.id}/{0.icon}.jpg'.format(self) - - @property - def created_at(self): - """Returns the channel's creation time in UTC.""" - return utils.snowflake_time(self.id) - - def permissions_for(self, user): - """Handles permission resolution for a :class:`User`. - - This function is there for compatibility with other channel types. - - Actual direct messages do not really have the concept of permissions. - - This returns all the Text related permissions set to true except: - - - send_tts_messages: You cannot send TTS messages in a DM. - - manage_messages: You cannot delete others messages in a DM. - - This also checks the kick_members permission if the user is the owner. - - Parameters - ----------- - user: :class:`User` - The user to check permissions for. - - Returns - -------- - :class:`Permissions` - The resolved permissions for the user. - """ - - base = Permissions.text() - base.send_tts_messages = False - base.manage_messages = False - base.mention_everyone = True - - if user.id == self.owner.id: - base.kick_members = True - - return base - - @asyncio.coroutine - def add_recipients(self, *recipients): - """|coro| - - Adds recipients to this group. - - A group can only have a maximum of 10 members. - Attempting to add more ends up in an exception. To - add a recipient to the group, you must have a relationship - with the user of type :attr:`RelationshipType.friend`. - - Parameters - ----------- - \*recipients: :class:`User` - An argument list of users to add to this group. - - Raises - ------- - HTTPException - Adding a recipient to this group failed. - """ - - # TODO: wait for the corresponding WS event - - req = self._state.http.add_group_recipient - for recipient in recipients: - yield from req(self.id, recipient.id) - - @asyncio.coroutine - def remove_recipients(self, *recipients): - """|coro| - - Removes recipients from this group. - - Parameters - ----------- - \*recipients: :class:`User` - An argument list of users to remove from this group. - - Raises - ------- - HTTPException - Removing a recipient from this group failed. - """ - - # TODO: wait for the corresponding WS event - - req = self._state.http.remove_group_recipient - for recipient in recipients: - yield from req(self.id, recipient.id) - - @asyncio.coroutine - def edit(self, **fields): - """|coro| - - Edits the group. - - Parameters - ----------- - name: Optional[str] - The new name to change the group to. - Could be ``None`` to remove the name. - icon: Optional[bytes] - A bytes-like object representing the new icon. - Could be ``None`` to remove the icon. - - Raises - ------- - HTTPException - Editing the group failed. - """ - - try: - icon_bytes = fields['icon'] - except KeyError: - pass - else: - if icon_bytes is not None: - fields['icon'] = utils._bytes_to_base64_data(icon_bytes) - - data = yield from self._state.http.edit_group(self.id, **fields) - self._update_group(data) - - @asyncio.coroutine - def leave(self): - """|coro| - - Leave the group. - - If you are the only one in the group, this deletes it as well. - - Raises - ------- - HTTPException - Leaving the group failed. - """ - - yield from self._state.http.leave_group(self.id) - -def _channel_factory(channel_type): - value = try_enum(ChannelType, channel_type) - if value is ChannelType.text: - return TextChannel, value - elif value is ChannelType.voice: - return VoiceChannel, value - elif value is ChannelType.private: - return DMChannel, value - elif value is ChannelType.category: - return CategoryChannel, value - elif value is ChannelType.group: - return GroupChannel, value - else: - return None, value diff --git a/discord.py-rewrite/discord/client.py b/discord.py-rewrite/discord/client.py deleted file mode 100644 index 9000e27..0000000 --- a/discord.py-rewrite/discord/client.py +++ /dev/null @@ -1,1055 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from .user import User, Profile -from .invite import Invite -from .object import Object -from .guild import Guild -from .errors import * -from .enums import Status, VoiceRegion -from .gateway import * -from .voice_client import VoiceClient -from .http import HTTPClient -from .state import ConnectionState -from . import utils, compat -from .backoff import ExponentialBackoff -from .webhook import Webhook - -import asyncio -import aiohttp -import websockets - -import logging, traceback -import sys, re -import signal -from collections import namedtuple - -PY35 = sys.version_info >= (3, 5) -log = logging.getLogger(__name__) - -AppInfo = namedtuple('AppInfo', 'id name description icon owner') - -def app_info_icon_url(self): - """Retrieves the application's icon_url if it exists. Empty string otherwise.""" - if not self.icon: - return '' - - return 'https://cdn.discordapp.com/app-icons/{0.id}/{0.icon}.jpg'.format(self) - -AppInfo.icon_url = property(app_info_icon_url) - -class Client: - """Represents a client connection that connects to Discord. - This class is used to interact with the Discord WebSocket and API. - - A number of options can be passed to the :class:`Client`. - - .. _event loop: https://docs.python.org/3/library/asyncio-eventloops.html - .. _connector: http://aiohttp.readthedocs.org/en/stable/client_reference.html#connectors - .. _ProxyConnector: http://aiohttp.readthedocs.org/en/stable/client_reference.html#proxyconnector - - Parameters - ---------- - max_messages : Optional[int] - The maximum number of messages to store in the internal message cache. - This defaults to 5000. Passing in `None` or a value less than 100 - will use the default instead of the passed in value. - loop : Optional[event loop] - The `event loop`_ to use for asynchronous operations. Defaults to ``None``, - in which case the default event loop is used via ``asyncio.get_event_loop()``. - connector : aiohttp.BaseConnector - The `connector`_ to use for connection pooling. - proxy : Optional[str] - Proxy URL. - proxy_auth : Optional[aiohttp.BasicAuth] - An object that represents proxy HTTP Basic Authorization. - shard_id : Optional[int] - Integer starting at 0 and less than shard_count. - shard_count : Optional[int] - The total number of shards. - fetch_offline_members: bool - Indicates if :func:`on_ready` should be delayed to fetch all offline - members from the guilds the bot belongs to. If this is ``False``\, then - no offline members are received and :meth:`request_offline_members` - must be used to fetch the offline members of the guild. - game: Optional[:class:`Game`] - A game to start your presence with upon logging on to Discord. - status: Optional[:class:`Status`] - A status to start your presence with upon logging on to Discord. - heartbeat_timeout: float - The maximum numbers of seconds before timing out and restarting the - WebSocket in the case of not receiving a HEARTBEAT_ACK. Useful if - processing the initial packets take too long to the point of disconnecting - you. The default timeout is 60 seconds. - - Attributes - ----------- - ws - The websocket gateway the client is currently connected to. Could be None. - loop - The `event loop`_ that the client uses for HTTP requests and websocket operations. - """ - def __init__(self, *, loop=None, **options): - self.ws = None - self.loop = asyncio.get_event_loop() if loop is None else loop - self._listeners = {} - self.shard_id = options.get('shard_id') - self.shard_count = options.get('shard_count') - - connector = options.pop('connector', None) - proxy = options.pop('proxy', None) - proxy_auth = options.pop('proxy_auth', None) - self.http = HTTPClient(connector, proxy=proxy, proxy_auth=proxy_auth, loop=self.loop) - - self._connection = ConnectionState(dispatch=self.dispatch, chunker=self._chunker, - syncer=self._syncer, http=self.http, loop=self.loop, **options) - - self._connection.shard_count = self.shard_count - self._closed = asyncio.Event(loop=self.loop) - self._ready = asyncio.Event(loop=self.loop) - self._connection._get_websocket = lambda g: self.ws - - if VoiceClient.warn_nacl: - VoiceClient.warn_nacl = False - log.warning("PyNaCl is not installed, voice will NOT be supported") - - # internals - - @asyncio.coroutine - def _syncer(self, guilds): - yield from self.ws.request_sync(guilds) - - @asyncio.coroutine - def _chunker(self, guild): - try: - guild_id = guild.id - except AttributeError: - guild_id = [s.id for s in guild] - - payload = { - 'op': 8, - 'd': { - 'guild_id': guild_id, - 'query': '', - 'limit': 0 - } - } - - yield from self.ws.send_as_json(payload) - - def handle_ready(self): - self._ready.set() - - def _resolve_invite(self, invite): - if isinstance(invite, Invite) or isinstance(invite, Object): - return invite.id - else: - rx = r'(?:https?\:\/\/)?discord\.gg\/(.+)' - m = re.match(rx, invite) - if m: - return m.group(1) - return invite - - @property - def latency(self): - """float: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. - - This could be referred to as the Discord WebSocket protocol latency. - """ - ws = self.ws - return float('nan') if not ws else ws.latency - - @property - def user(self): - """Optional[:class:`ClientUser`]: Represents the connected client. None if not logged in.""" - return self._connection.user - - @property - def guilds(self): - """List[:class:`Guild`]: The guilds that the connected client is a member of.""" - return self._connection.guilds - - @property - def emojis(self): - """List[:class:`Emoji`]: The emojis that the connected client has.""" - return self._connection.emojis - - @property - def private_channels(self): - """List[:class:`abc.PrivateChannel`]: The private channels that the connected client is participating on. - - .. note:: - - This returns only up to 128 most recent private channels due to an internal working - on how Discord deals with private channels. - """ - return self._connection.private_channels - - @property - def voice_clients(self): - """List[:class:`VoiceClient`]: Represents a list of voice connections.""" - return self._connection.voice_clients - - def is_ready(self): - """bool: Specifies if the client's internal cache is ready for use.""" - return self._ready.is_set() - - @asyncio.coroutine - def _run_event(self, coro, event_name, *args, **kwargs): - try: - yield from coro(*args, **kwargs) - except asyncio.CancelledError: - pass - except Exception: - try: - yield from self.on_error(event_name, *args, **kwargs) - except asyncio.CancelledError: - pass - - def dispatch(self, event, *args, **kwargs): - log.debug('Dispatching event %s', event) - method = 'on_' + event - handler = 'handle_' + event - - listeners = self._listeners.get(event) - if listeners: - removed = [] - for i, (future, condition) in enumerate(listeners): - if future.cancelled(): - removed.append(i) - continue - - try: - result = condition(*args) - except Exception as e: - future.set_exception(e) - removed.append(i) - else: - if result: - if len(args) == 0: - future.set_result(None) - elif len(args) == 1: - future.set_result(args[0]) - else: - future.set_result(args) - removed.append(i) - - if len(removed) == len(listeners): - self._listeners.pop(event) - else: - for idx in reversed(removed): - del listeners[idx] - - try: - actual_handler = getattr(self, handler) - except AttributeError: - pass - else: - actual_handler(*args, **kwargs) - - try: - coro = getattr(self, method) - except AttributeError: - pass - else: - compat.create_task(self._run_event(coro, method, *args, **kwargs), loop=self.loop) - - @asyncio.coroutine - def on_error(self, event_method, *args, **kwargs): - """|coro| - - The default error handler provided by the client. - - By default this prints to ``sys.stderr`` however it could be - overridden to have a different implementation. - Check :func:`discord.on_error` for more details. - """ - print('Ignoring exception in {}'.format(event_method), file=sys.stderr) - traceback.print_exc() - - @asyncio.coroutine - def request_offline_members(self, *guilds): - """|coro| - - Requests previously offline members from the guild to be filled up - into the :attr:`Guild.members` cache. This function is usually not - called. It should only be used if you have the ``fetch_offline_members`` - parameter set to ``False``. - - When the client logs on and connects to the websocket, Discord does - not provide the library with offline members if the number of members - in the guild is larger than 250. You can check if a guild is large - if :attr:`Guild.large` is ``True``. - - Parameters - ----------- - \*guilds - An argument list of guilds to request offline members for. - - Raises - ------- - InvalidArgument - If any guild is unavailable or not large in the collection. - """ - if any(not g.large or g.unavailable for g in guilds): - raise InvalidArgument('An unavailable or non-large guild was passed.') - - yield from self._connection.request_offline_members(guilds) - - # login state management - - @asyncio.coroutine - def login(self, token, *, bot=True): - """|coro| - - Logs in the client with the specified credentials. - - This function can be used in two different ways. - - Parameters - ----------- - token: str - The authentication token. Do not prefix this token with - anything as the library will do it for you. - bot: bool - Keyword argument that specifies if the account logging on is a bot - token or not. - - Raises - ------ - LoginFailure - The wrong credentials are passed. - HTTPException - An unknown HTTP related error occurred, - usually when it isn't 200 or the known incorrect credentials - passing status code. - """ - - log.info('logging in using static token') - yield from self.http.static_login(token, bot=bot) - self._connection.is_bot = bot - - @asyncio.coroutine - def logout(self): - """|coro| - - Logs out of Discord and closes all connections. - """ - yield from self.close() - - @asyncio.coroutine - def _connect(self): - coro = DiscordWebSocket.from_client(self, shard_id=self.shard_id) - self.ws = yield from asyncio.wait_for(coro, timeout=180.0, loop=self.loop) - while True: - try: - yield from self.ws.poll_event() - except ResumeWebSocket as e: - log.info('Got a request to RESUME the websocket.') - coro = DiscordWebSocket.from_client(self, shard_id=self.shard_id, - session=self.ws.session_id, - sequence=self.ws.sequence, - resume=True) - self.ws = yield from asyncio.wait_for(coro, timeout=180.0, loop=self.loop) - - @asyncio.coroutine - def connect(self, *, reconnect=True): - """|coro| - - Creates a websocket connection and lets the websocket listen - to messages from discord. This is a loop that runs the entire - event system and miscellaneous aspects of the library. Control - is not resumed until the WebSocket connection is terminated. - - Parameters - ----------- - reconnect: bool - If we should attempt reconnecting, either due to internet - failure or a specific failure on Discord's part. Certain - disconnects that lead to bad state will not be handled (such as - invalid sharding payloads or bad tokens). - - Raises - ------- - GatewayNotFound - If the gateway to connect to discord is not found. Usually if this - is thrown then there is a discord API outage. - ConnectionClosed - The websocket connection has been terminated. - """ - - backoff = ExponentialBackoff() - while not self.is_closed(): - try: - yield from self._connect() - except (OSError, - HTTPException, - GatewayNotFound, - ConnectionClosed, - aiohttp.ClientError, - asyncio.TimeoutError, - websockets.InvalidHandshake, - websockets.WebSocketProtocolError) as e: - - if not reconnect: - yield from self.close() - if isinstance(e, ConnectionClosed) and e.code == 1000: - # clean close, don't re-raise this - return - raise - - if self.is_closed(): - return - - # We should only get this when an unhandled close code happens, - # such as a clean disconnect (1000) or a bad state (bad token, no sharding, etc) - # sometimes, discord sends us 1000 for unknown reasons so we should reconnect - # regardless and rely on is_closed instead - if isinstance(e, ConnectionClosed): - if e.code != 1000: - yield from self.close() - raise - - retry = backoff.delay() - log.exception("Attempting a reconnect in %.2fs", retry) - yield from asyncio.sleep(retry, loop=self.loop) - - @asyncio.coroutine - def close(self): - """|coro| - - Closes the connection to discord. - """ - if self.is_closed(): - return - - self._closed.set() - - for voice in self.voice_clients: - try: - yield from voice.disconnect() - except: - # if an error happens during disconnects, disregard it. - pass - - if self.ws is not None and self.ws.open: - yield from self.ws.close() - - - yield from self.http.close() - self._ready.clear() - - @asyncio.coroutine - def start(self, *args, **kwargs): - """|coro| - - A shorthand coroutine for :meth:`login` + :meth:`connect`. - """ - - bot = kwargs.pop('bot', True) - reconnect = kwargs.pop('reconnect', True) - yield from self.login(*args, bot=bot) - yield from self.connect(reconnect=reconnect) - - def _do_cleanup(self): - log.info('Cleaning up event loop.') - loop = self.loop - if loop.is_closed(): - return # we're already cleaning up - - task = compat.create_task(self.close(), loop=loop) - - def _silence_gathered(fut): - try: - fut.result() - except: - pass - finally: - loop.stop() - - def when_future_is_done(fut): - pending = asyncio.Task.all_tasks(loop=loop) - if pending: - log.info('Cleaning up after %s tasks', len(pending)) - gathered = asyncio.gather(*pending, loop=loop) - gathered.cancel() - gathered.add_done_callback(_silence_gathered) - else: - loop.stop() - - task.add_done_callback(when_future_is_done) - if not loop.is_running(): - loop.run_forever() - else: - # on Linux, we're still running because we got triggered via - # the signal handler rather than the natural KeyboardInterrupt - # Since that's the case, we're going to return control after - # registering the task for the event loop to handle later - return None - - try: - return task.result() # suppress unused task warning - except: - return None - - def run(self, *args, **kwargs): - """A blocking call that abstracts away the `event loop`_ - initialisation from you. - - If you want more control over the event loop then this - function should not be used. Use :meth:`start` coroutine - or :meth:`connect` + :meth:`login`. - - Roughly Equivalent to: :: - - try: - loop.run_until_complete(start(*args, **kwargs)) - except KeyboardInterrupt: - loop.run_until_complete(logout()) - # cancel all tasks lingering - finally: - loop.close() - - Warning - -------- - This function must be the last function to call due to the fact that it - is blocking. That means that registration of events or anything being - called after this function call will not execute until it returns. - """ - is_windows = sys.platform == 'win32' - loop = self.loop - if not is_windows: - loop.add_signal_handler(signal.SIGINT, self._do_cleanup) - loop.add_signal_handler(signal.SIGTERM, self._do_cleanup) - - task = compat.create_task(self.start(*args, **kwargs), loop=loop) - - def stop_loop_on_finish(fut): - loop.stop() - - task.add_done_callback(stop_loop_on_finish) - - try: - loop.run_forever() - except KeyboardInterrupt: - log.info('Received signal to terminate bot and event loop.') - finally: - task.remove_done_callback(stop_loop_on_finish) - if is_windows: - self._do_cleanup() - - loop.close() - if task.cancelled() or not task.done(): - return None - return task.result() - - # properties - - def is_closed(self): - """bool: Indicates if the websocket connection is closed.""" - return self._closed.is_set() - - # helpers/getters - - @property - def users(self): - """Returns a list of all the :class:`User` the bot can see.""" - return list(self._connection._users.values()) - - def get_channel(self, id): - """Returns a :class:`abc.GuildChannel` or :class:`abc.PrivateChannel` with the following ID. - - If not found, returns None. - """ - return self._connection.get_channel(id) - - def get_guild(self, id): - """Returns a :class:`Guild` with the given ID. If not found, returns None.""" - return self._connection._get_guild(id) - - def get_user(self, id): - """Returns a :class:`User` with the given ID. If not found, returns None.""" - return self._connection.get_user(id) - - def get_emoji(self, id): - """Returns a :class:`Emoji` with the given ID. If not found, returns None.""" - return self._connection.get_emoji(id) - - def get_all_channels(self): - """A generator that retrieves every :class:`abc.GuildChannel` the client can 'access'. - - This is equivalent to: :: - - for guild in client.guilds: - for channel in guild.channels: - yield channel - - Note - ----- - Just because you receive a :class:`abc.GuildChannel` does not mean that - you can communicate in said channel. :meth:`abc.GuildChannel.permissions_for` should - be used for that. - """ - - for guild in self.guilds: - for channel in guild.channels: - yield channel - - def get_all_members(self): - """Returns a generator with every :class:`Member` the client can see. - - This is equivalent to: :: - - for guild in client.guilds: - for member in guild.members: - yield member - - """ - for guild in self.guilds: - for member in guild.members: - yield member - - # listeners/waiters - - @asyncio.coroutine - def wait_until_ready(self): - """|coro| - - Waits until the client's internal cache is all ready. - """ - yield from self._ready.wait() - - def wait_for(self, event, *, check=None, timeout=None): - """|coro| - - Waits for a WebSocket event to be dispatched. - - This could be used to wait for a user to reply to a message, - or to react to a message, or to edit a message in a self-contained - way. - - The ``timeout`` parameter is passed onto `asyncio.wait_for`_. By default, - it does not timeout. Note that this does propagate the - ``asyncio.TimeoutError`` for you in case of timeout and is provided for - ease of use. - - In case the event returns multiple arguments, a tuple containing those - arguments is returned instead. Please check the - :ref:`documentation ` for a list of events and their - parameters. - - This function returns the **first event that meets the requirements**. - - .. _asyncio.wait_for: https://docs.python.org/3/library/asyncio-task.html#asyncio.wait_for - - Examples - --------- - - Waiting for a user reply: :: - - @client.event - async def on_message(message): - if message.content.startswith('$greet'): - channel = message.channel - await channel.send('Say hello!') - - def check(m): - return m.content == 'hello' and m.channel == channel - - msg = await client.wait_for('message', check=check) - await channel.send('Hello {.author}!'.format(msg)) - - Waiting for a thumbs up reaction from the message author: :: - - @client.event - async def on_message(message): - if message.content.startswith('$thumb'): - channel = message.channel - await channel.send('Send me that \N{THUMBS UP SIGN} reaction, mate') - - def check(reaction, user): - return user == message.author and str(reaction.emoji) == '\N{THUMBS UP SIGN}' - - try: - reaction, user = await client.wait_for('reaction_add', timeout=60.0, check=check) - except asyncio.TimeoutError: - await channel.send('\N{THUMBS DOWN SIGN}') - else: - await channel.send('\N{THUMBS UP SIGN}') - - - Parameters - ------------ - event: str - The event name, similar to the :ref:`event reference `, - but without the ``on_`` prefix, to wait for. - check: Optional[predicate] - A predicate to check what to wait for. The arguments must meet the - parameters of the event being waited for. - timeout: Optional[float] - The number of seconds to wait before timing out and raising - ``asyncio.TimeoutError``\. - - Raises - ------- - asyncio.TimeoutError - If a timeout is provided and it was reached. - - Returns - -------- - Any - Returns no arguments, a single argument, or a tuple of multiple - arguments that mirrors the parameters passed in the - :ref:`event reference `. - """ - - future = compat.create_future(self.loop) - if check is None: - def _check(*args): - return True - check = _check - - ev = event.lower() - try: - listeners = self._listeners[ev] - except KeyError: - listeners = [] - self._listeners[ev] = listeners - - listeners.append((future, check)) - return asyncio.wait_for(future, timeout, loop=self.loop) - - # event registration - - def event(self, coro): - """A decorator that registers an event to listen to. - - You can find more info about the events on the :ref:`documentation below `. - - The events must be a |corourl|_, if not, :exc:`ClientException` is raised. - - Examples - --------- - - Using the basic :meth:`event` decorator: :: - - @client.event - @asyncio.coroutine - def on_ready(): - print('Ready!') - - Saving characters by using the :meth:`async_event` decorator: :: - - @client.async_event - def on_ready(): - print('Ready!') - - """ - - if not asyncio.iscoroutinefunction(coro): - raise ClientException('event registered must be a coroutine function') - - setattr(self, coro.__name__, coro) - log.info('%s has successfully been registered as an event', coro.__name__) - return coro - - def async_event(self, coro): - """A shorthand decorator for ``asyncio.coroutine`` + :meth:`event`.""" - if not asyncio.iscoroutinefunction(coro): - coro = asyncio.coroutine(coro) - - return self.event(coro) - - @asyncio.coroutine - def change_presence(self, *, game=None, status=None, afk=False): - """|coro| - - Changes the client's presence. - - The game parameter is a Game object (not a string) that represents - a game being played currently. - - Example: :: - - game = discord.Game(name="with the API") - await client.change_presence(status=discord.Status.idle, game=game) - - Parameters - ---------- - game: Optional[:class:`Game`] - The game being played. None if no game is being played. - status: Optional[:class:`Status`] - Indicates what status to change to. If None, then - :attr:`Status.online` is used. - afk: bool - Indicates if you are going AFK. This allows the discord - client to know how to handle push notifications better - for you in case you are actually idle and not lying. - - Raises - ------ - InvalidArgument - If the ``game`` parameter is not :class:`Game` or None. - """ - - if status is None: - status = 'online' - status_enum = Status.online - elif status is Status.offline: - status = 'invisible' - status_enum = Status.offline - else: - status_enum = status - status = str(status) - - yield from self.ws.change_presence(game=game, status=status, afk=afk) - - for guild in self._connection.guilds: - me = guild.me - if me is None: - continue - - me.game = game - me.status = status_enum - - # Guild stuff - - @asyncio.coroutine - def create_guild(self, name, region=None, icon=None): - """|coro| - - Creates a :class:`Guild`. - - Bot accounts generally are not allowed to create servers. - - Parameters - ---------- - name: str - The name of the guild. - region: :class:`VoiceRegion` - The region for the voice communication server. - Defaults to :attr:`VoiceRegion.us_west`. - icon: bytes - The *bytes-like* object representing the icon. See :meth:`~ClientUser.edit` - for more details on what is expected. - - Raises - ------ - HTTPException - Guild creation failed. - InvalidArgument - Invalid icon image format given. Must be PNG or JPG. - - Returns - ------- - :class:`Guild` - The guild created. This is not the same guild that is - added to cache. - """ - if icon is not None: - icon = utils._bytes_to_base64_data(icon) - - if region is None: - region = VoiceRegion.us_west.value - else: - region = region.value - - data = yield from self.http.create_guild(name, region, icon) - return Guild(data=data, state=self._connection) - - # Invite management - - @asyncio.coroutine - def get_invite(self, url): - """|coro| - - Gets a :class:`Invite` from a discord.gg URL or ID. - - Note - ------ - If the invite is for a guild you have not joined, the guild and channel - attributes of the returned invite will be :class:`Object` with the names - patched in. - - Parameters - ----------- - url : str - The discord invite ID or URL (must be a discord.gg URL). - - Raises - ------- - NotFound - The invite has expired or is invalid. - HTTPException - Getting the invite failed. - - Returns - -------- - :class:`Invite` - The invite from the URL/ID. - """ - - invite_id = self._resolve_invite(url) - data = yield from self.http.get_invite(invite_id) - return Invite.from_incomplete(state=self._connection, data=data) - - @asyncio.coroutine - def delete_invite(self, invite): - """|coro| - - Revokes an :class:`Invite`, URL, or ID to an invite. - - Parameters - ---------- - invite - The invite to revoke. - - Raises - ------- - Forbidden - You do not have permissions to revoke invites. - NotFound - The invite is invalid or expired. - HTTPException - Revoking the invite failed. - """ - - invite_id = self._resolve_invite(invite) - yield from self.http.delete_invite(invite_id) - - # Miscellaneous stuff - - @asyncio.coroutine - def application_info(self): - """|coro| - - Retrieve's the bot's application information. - - Returns - -------- - :class:`AppInfo` - A namedtuple representing the application info. - - Raises - ------- - HTTPException - Retrieving the information failed somehow. - """ - data = yield from self.http.application_info() - return AppInfo(id=data['id'], name=data['name'], - description=data['description'], icon=data['icon'], - owner=User(state=self._connection, data=data['owner'])) - - @asyncio.coroutine - def get_user_info(self, user_id): - """|coro| - - Retrieves a :class:`User` based on their ID. This can only - be used by bot accounts. You do not have to share any guilds - with the user to get this information, however many operations - do require that you do. - - Parameters - ----------- - user_id: int - The user's ID to fetch from. - - Returns - -------- - :class:`User` - The user you requested. - - Raises - ------- - NotFound - A user with this ID does not exist. - HTTPException - Fetching the user failed. - """ - data = yield from self.http.get_user_info(user_id) - return User(state=self._connection, data=data) - - @asyncio.coroutine - def get_user_profile(self, user_id): - """|coro| - - Gets an arbitrary user's profile. This can only be used by non-bot accounts. - - Parameters - ------------ - user_id: int - The ID of the user to fetch their profile for. - - Raises - ------- - Forbidden - Not allowed to fetch profiles. - HTTPException - Fetching the profile failed. - - Returns - -------- - :class:`Profile` - The profile of the user. - """ - - state = self._connection - data = yield from self.http.get_user_profile(user_id) - - def transform(d): - return state._get_guild(int(d['id'])) - - since = data.get('premium_since') - mutual_guilds = list(filter(None, map(transform, data.get('mutual_guilds', [])))) - user = data['user'] - return Profile(flags=user.get('flags', 0), - premium_since=utils.parse_time(since), - mutual_guilds=mutual_guilds, - user=User(data=user, state=state), - connected_accounts=data['connected_accounts']) - - @asyncio.coroutine - def get_webhook_info(self, webhook_id): - """|coro| - - Retrieves a :class:`Webhook` with the specified ID. - - Raises - -------- - HTTPException - Retrieving the webhook failed. - NotFound - Invalid webhook ID. - Forbidden - You do not have permission to fetch this webhook. - - Returns - --------- - :class:`Webhook` - The webhook you requested. - """ - data = yield from self.http.get_webhook(webhook_id) - return Webhook.from_state(data, state=self._connection) diff --git a/discord.py-rewrite/discord/colour.py b/discord.py-rewrite/discord/colour.py deleted file mode 100644 index 9644b27..0000000 --- a/discord.py-rewrite/discord/colour.py +++ /dev/null @@ -1,222 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -class Colour: - """Represents a Discord role colour. This class is similar - to an (red, green, blue) tuple. - - There is an alias for this called Color. - - .. container:: operations - - .. describe:: x == y - - Checks if two colours are equal. - - .. describe:: x != y - - Checks if two colours are not equal. - - .. describe:: hash(x) - - Return the colour's hash. - - .. describe:: str(x) - - Returns the hex format for the colour. - - Attributes - ------------ - value: int - The raw integer colour value. - """ - - __slots__ = ('value',) - - def __init__(self, value): - if not isinstance(value, int): - raise TypeError('Expected int parameter, received %s instead.' % value.__class__.__name__) - - self.value = value - - def _get_byte(self, byte): - return (self.value >> (8 * byte)) & 0xff - - def __eq__(self, other): - return isinstance(other, Colour) and self.value == other.value - - def __ne__(self, other): - return not self.__eq__(other) - - def __str__(self): - return '#{:0>6x}'.format(self.value) - - def __repr__(self): - return '' % self.value - - def __hash__(self): - return hash(self.value) - - @property - def r(self): - """Returns the red component of the colour.""" - return self._get_byte(2) - - @property - def g(self): - """Returns the green component of the colour.""" - return self._get_byte(1) - - @property - def b(self): - """Returns the blue component of the colour.""" - return self._get_byte(0) - - def to_rgb(self): - """Returns an (r, g, b) tuple representing the colour.""" - return (self.r, self.g, self.b) - - @classmethod - def from_rgb(cls, r, g, b): - """Constructs a :class:`Colour` from an RGB tuple.""" - return cls((r << 16) + (g << 8) + b) - - @classmethod - def default(cls): - """A factory method that returns a :class:`Colour` with a value of 0.""" - return cls(0) - - @classmethod - def teal(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x1abc9c``.""" - return cls(0x1abc9c) - - @classmethod - def dark_teal(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x11806a``.""" - return cls(0x11806a) - - @classmethod - def green(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x2ecc71``.""" - return cls(0x2ecc71) - - @classmethod - def dark_green(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x1f8b4c``.""" - return cls(0x1f8b4c) - - @classmethod - def blue(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x3498db``.""" - return cls(0x3498db) - - @classmethod - def dark_blue(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x206694``.""" - return cls(0x206694) - - @classmethod - def purple(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x9b59b6``.""" - return cls(0x9b59b6) - - @classmethod - def dark_purple(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x71368a``.""" - return cls(0x71368a) - - @classmethod - def magenta(cls): - """A factory method that returns a :class:`Colour` with a value of ``0xe91e63``.""" - return cls(0xe91e63) - - @classmethod - def dark_magenta(cls): - """A factory method that returns a :class:`Colour` with a value of ``0xad1457``.""" - return cls(0xad1457) - - @classmethod - def gold(cls): - """A factory method that returns a :class:`Colour` with a value of ``0xf1c40f``.""" - return cls(0xf1c40f) - - @classmethod - def dark_gold(cls): - """A factory method that returns a :class:`Colour` with a value of ``0xc27c0e``.""" - return cls(0xc27c0e) - - @classmethod - def orange(cls): - """A factory method that returns a :class:`Colour` with a value of ``0xe67e22``.""" - return cls(0xe67e22) - - @classmethod - def dark_orange(cls): - """A factory method that returns a :class:`Colour` with a value of ``0xa84300``.""" - return cls(0xa84300) - - @classmethod - def red(cls): - """A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``.""" - return cls(0xe74c3c) - - @classmethod - def dark_red(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x992d22``.""" - return cls(0x992d22) - - @classmethod - def lighter_grey(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x95a5a6``.""" - return cls(0x95a5a6) - - @classmethod - def dark_grey(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x607d8b``.""" - return cls(0x607d8b) - - @classmethod - def light_grey(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x979c9f``.""" - return cls(0x979c9f) - - @classmethod - def darker_grey(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x546e7a``.""" - return cls(0x546e7a) - - @classmethod - def blurple(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x7289da``.""" - return cls(0x7289da) - - @classmethod - def greyple(cls): - """A factory method that returns a :class:`Colour` with a value of ``0x99aab5``.""" - return cls(0x99aab5) - -Color = Colour diff --git a/discord.py-rewrite/discord/compat.py b/discord.py-rewrite/discord/compat.py deleted file mode 100644 index 3b9e66f..0000000 --- a/discord.py-rewrite/discord/compat.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf-8 -*- -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import concurrent.futures -import asyncio - -try: - create_task = asyncio.ensure_future -except AttributeError: - create_task = asyncio.async - -try: - _create_future = asyncio.AbstractEventLoop.create_future -except AttributeError: - def create_future(loop): - return asyncio.Future(loop=loop) -else: - def create_future(loop): - return loop.create_future() - -try: - run_coroutine_threadsafe = asyncio.run_coroutine_threadsafe -except AttributeError: - # the following code is slightly modified from the - # official asyncio repository that could be found here: - # https://github.com/python/asyncio/blob/master/asyncio/futures.py - # with a commit hash of 5c7efbcdfbe6a5c25b4cd5df22d9a15ab4062c8e - # this portion is licensed under Apache license 2.0 - - def _set_concurrent_future_state(concurrent, source): - """Copy state from a future to a concurrent.futures.Future.""" - assert source.done() - if source.cancelled(): - concurrent.cancel() - if not concurrent.set_running_or_notify_cancel(): - return - exception = source.exception() - if exception is not None: - concurrent.set_exception(exception) - else: - result = source.result() - concurrent.set_result(result) - - - def _copy_future_state(source, dest): - """Internal helper to copy state from another Future. - The other Future may be a concurrent.futures.Future. - """ - assert source.done() - if dest.cancelled(): - return - assert not dest.done() - if source.cancelled(): - dest.cancel() - else: - exception = source.exception() - if exception is not None: - dest.set_exception(exception) - else: - result = source.result() - dest.set_result(result) - - def _chain_future(source, destination): - """Chain two futures so that when one completes, so does the other. - The result (or exception) of source will be copied to destination. - If destination is cancelled, source gets cancelled too. - Compatible with both asyncio.Future and concurrent.futures.Future. - """ - if not isinstance(source, (asyncio.Future, concurrent.futures.Future)): - raise TypeError('A future is required for source argument') - - if not isinstance(destination, (asyncio.Future, concurrent.futures.Future)): - raise TypeError('A future is required for destination argument') - - source_loop = source._loop if isinstance(source, asyncio.Future) else None - dest_loop = destination._loop if isinstance(destination, asyncio.Future) else None - - def _set_state(future, other): - if isinstance(future, asyncio.Future): - _copy_future_state(other, future) - else: - _set_concurrent_future_state(future, other) - - def _call_check_cancel(destination): - if destination.cancelled(): - if source_loop is None or source_loop is dest_loop: - source.cancel() - else: - source_loop.call_soon_threadsafe(source.cancel) - - def _call_set_state(source): - if dest_loop is None or dest_loop is source_loop: - _set_state(destination, source) - else: - dest_loop.call_soon_threadsafe(_set_state, destination, source) - - destination.add_done_callback(_call_check_cancel) - source.add_done_callback(_call_set_state) - - def run_coroutine_threadsafe(coro, loop): - """Submit a coroutine object to a given event loop. - - Return a concurrent.futures.Future to access the result. - """ - if not asyncio.iscoroutine(coro): - raise TypeError('A coroutine object is required') - - future = concurrent.futures.Future() - - def callback(): - try: - _chain_future(create_task(coro, loop=loop), future) - except Exception as exc: - if future.set_running_or_notify_cancel(): - future.set_exception(exc) - raise - loop.call_soon_threadsafe(callback) - return future diff --git a/discord.py-rewrite/discord/context_managers.py b/discord.py-rewrite/discord/context_managers.py deleted file mode 100644 index 93d292c..0000000 --- a/discord.py-rewrite/discord/context_managers.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import asyncio - -from .compat import create_task - -def _typing_done_callback(fut): - # just retrieve any exception and call it a day - try: - fut.exception() - except: - pass - -class Typing: - def __init__(self, messageable): - self.loop = messageable._state.loop - self.messageable = messageable - - @asyncio.coroutine - def do_typing(self): - try: - channel = self._channel - except AttributeError: - channel = yield from self.messageable._get_channel() - - typing = channel._state.http.send_typing - - while True: - yield from typing(channel.id) - yield from asyncio.sleep(5) - - def __enter__(self): - self.task = create_task(self.do_typing(), loop=self.loop) - self.task.add_done_callback(_typing_done_callback) - return self - - def __exit__(self, exc_type, exc, tb): - self.task.cancel() - - @asyncio.coroutine - def __aenter__(self): - self._channel = channel = yield from self.messageable._get_channel() - yield from channel._state.http.send_typing(channel.id) - return self.__enter__() - - @asyncio.coroutine - def __aexit__(self, exc_type, exc, tb): - self.task.cancel() diff --git a/discord.py-rewrite/discord/embeds.py b/discord.py-rewrite/discord/embeds.py deleted file mode 100644 index 77d0c7b..0000000 --- a/discord.py-rewrite/discord/embeds.py +++ /dev/null @@ -1,478 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import datetime - -from . import utils -from .colour import Colour - -class _EmptyEmbed: - def __bool__(self): - return False - - def __repr__(self): - return 'Embed.Empty' - -EmptyEmbed = _EmptyEmbed() - -class EmbedProxy: - def __init__(self, layer): - self.__dict__.update(layer) - - def __len__(self): - return len(self.__dict__) - - def __repr__(self): - return 'EmbedProxy(%s)' % ', '.join(('%s=%r' % (k, v) for k, v in self.__dict__.items() if not k.startswith('_'))) - - def __getattr__(self, attr): - return EmptyEmbed - -class Embed: - """Represents a Discord embed. - - The following attributes can be set during creation - of the object: - - Certain properties return an ``EmbedProxy``. Which is a type - that acts similar to a regular `dict` except access the attributes - via dotted access, e.g. ``embed.author.icon_url``. If the attribute - is invalid or empty, then a special sentinel value is returned, - :attr:`Embed.Empty`. - - For ease of use, all parameters that expect a ``str`` are implicitly - casted to ``str`` for you. - - Attributes - ----------- - title: str - The title of the embed. - type: str - The type of embed. Usually "rich". - description: str - The description of the embed. - url: str - The URL of the embed. - timestamp: `datetime.datetime` - The timestamp of the embed content. This could be a naive or aware datetime. - colour: :class:`Colour` or int - The colour code of the embed. Aliased to ``color`` as well. - Empty - A special sentinel value used by ``EmbedProxy`` and this class - to denote that the value or attribute is empty. - """ - - __slots__ = ('title', 'url', 'type', '_timestamp', '_colour', '_footer', - '_image', '_thumbnail', '_video', '_provider', '_author', - '_fields', 'description') - - Empty = EmptyEmbed - - def __init__(self, **kwargs): - # swap the colour/color aliases - try: - colour = kwargs['colour'] - except KeyError: - colour = kwargs.get('color', EmptyEmbed) - - self.colour = colour - self.title = kwargs.get('title', EmptyEmbed) - self.type = kwargs.get('type', 'rich') - self.url = kwargs.get('url', EmptyEmbed) - self.description = kwargs.get('description', EmptyEmbed) - - try: - timestamp = kwargs['timestamp'] - except KeyError: - pass - else: - self.timestamp = timestamp - - @classmethod - def from_data(cls, data): - # we are bypassing __init__ here since it doesn't apply here - self = cls.__new__(cls) - - # fill in the basic fields - - self.title = data.get('title', EmptyEmbed) - self.type = data.get('type', EmptyEmbed) - self.description = data.get('description', EmptyEmbed) - self.url = data.get('url', EmptyEmbed) - - # try to fill in the more rich fields - - try: - self._colour = Colour(value=data['color']) - except KeyError: - pass - - try: - self._timestamp = utils.parse_time(data['timestamp']) - except KeyError: - pass - - for attr in ('thumbnail', 'video', 'provider', 'author', 'fields', 'image', 'footer'): - try: - value = data[attr] - except KeyError: - continue - else: - setattr(self, '_' + attr, value) - - return self - - @property - def colour(self): - return getattr(self, '_colour', EmptyEmbed) - - @colour.setter - def colour(self, value): - if isinstance(value, (Colour, _EmptyEmbed)): - self._colour = value - elif isinstance(value, int): - self._colour = Colour(value=value) - else: - raise TypeError('Expected discord.Colour, int, or Embed.Empty but received %s instead.' % value.__class__.__name__) - - color = colour - - @property - def timestamp(self): - return getattr(self, '_timestamp', EmptyEmbed) - - @timestamp.setter - def timestamp(self, value): - if isinstance(value, (datetime.datetime, _EmptyEmbed)): - self._timestamp = value - else: - raise TypeError("Expected datetime.datetime or Embed.Empty received %s instead" % value.__class__.__name__) - - @property - def footer(self): - """Returns an ``EmbedProxy`` denoting the footer contents. - - See :meth:`set_footer` for possible values you can access. - - If the attribute has no value then :attr:`Empty` is returned. - """ - return EmbedProxy(getattr(self, '_footer', {})) - - def set_footer(self, *, text=EmptyEmbed, icon_url=EmptyEmbed): - """Sets the footer for the embed content. - - This function returns the class instance to allow for fluent-style - chaining. - - Parameters - ----------- - text: str - The footer text. - icon_url: str - The URL of the footer icon. Only HTTP(S) is supported. - """ - - self._footer = {} - if text is not EmptyEmbed: - self._footer['text'] = str(text) - - if icon_url is not EmptyEmbed: - self._footer['icon_url'] = str(icon_url) - - return self - - @property - def image(self): - """Returns an ``EmbedProxy`` denoting the image contents. - - Possible attributes you can access are: - - - ``url`` - - ``proxy_url`` - - ``width`` - - ``height`` - - If the attribute has no value then :attr:`Empty` is returned. - """ - return EmbedProxy(getattr(self, '_image', {})) - - def set_image(self, *, url): - """Sets the image for the embed content. - - This function returns the class instance to allow for fluent-style - chaining. - - Parameters - ----------- - url: str - The source URL for the image. Only HTTP(S) is supported. - """ - - self._image = { - 'url': str(url) - } - - return self - - @property - def thumbnail(self): - """Returns an ``EmbedProxy`` denoting the thumbnail contents. - - Possible attributes you can access are: - - - ``url`` - - ``proxy_url`` - - ``width`` - - ``height`` - - If the attribute has no value then :attr:`Empty` is returned. - """ - return EmbedProxy(getattr(self, '_thumbnail', {})) - - def set_thumbnail(self, *, url): - """Sets the thumbnail for the embed content. - - This function returns the class instance to allow for fluent-style - chaining. - - Parameters - ----------- - url: str - The source URL for the thumbnail. Only HTTP(S) is supported. - """ - - self._thumbnail = { - 'url': str(url) - } - - return self - - @property - def video(self): - """Returns an ``EmbedProxy`` denoting the video contents. - - Possible attributes include: - - - ``url`` for the video URL. - - ``height`` for the video height. - - ``width`` for the video width. - - If the attribute has no value then :attr:`Empty` is returned. - """ - return EmbedProxy(getattr(self, '_video', {})) - - @property - def provider(self): - """Returns an ``EmbedProxy`` denoting the provider contents. - - The only attributes that might be accessed are ``name`` and ``url``. - - If the attribute has no value then :attr:`Empty` is returned. - """ - return EmbedProxy(getattr(self, '_provider', {})) - - @property - def author(self): - """Returns an ``EmbedProxy`` denoting the author contents. - - See :meth:`set_author` for possible values you can access. - - If the attribute has no value then :attr:`Empty` is returned. - """ - return EmbedProxy(getattr(self, '_author', {})) - - def set_author(self, *, name, url=EmptyEmbed, icon_url=EmptyEmbed): - """Sets the author for the embed content. - - This function returns the class instance to allow for fluent-style - chaining. - - Parameters - ----------- - name: str - The name of the author. - url: str - The URL for the author. - icon_url: str - The URL of the author icon. Only HTTP(S) is supported. - """ - - self._author = { - 'name': str(name) - } - - if url is not EmptyEmbed: - self._author['url'] = str(url) - - if icon_url is not EmptyEmbed: - self._author['icon_url'] = str(icon_url) - - return self - - @property - def fields(self): - """Returns a list of ``EmbedProxy`` denoting the field contents. - - See :meth:`add_field` for possible values you can access. - - If the attribute has no value then :attr:`Empty` is returned. - """ - return [EmbedProxy(d) for d in getattr(self, '_fields', [])] - - def add_field(self, *, name, value, inline=True): - """Adds a field to the embed object. - - This function returns the class instance to allow for fluent-style - chaining. - - Parameters - ----------- - name: str - The name of the field. - value: str - The value of the field. - inline: bool - Whether the field should be displayed inline. - """ - - field = { - 'inline': inline, - 'name': str(name), - 'value': str(value) - } - - try: - self._fields.append(field) - except AttributeError: - self._fields = [field] - - return self - - def clear_fields(self): - """Removes all fields from this embed.""" - try: - self._fields.clear() - except AttributeError: - self._fields = [] - - def remove_field(self, index): - """Removes a field at a specified index. - - If the index is invalid or out of bounds then the error is - silently swallowed. - - .. note:: - - When deleting a field by index, the index of the other fields - shift to fill the gap just like a regular list. - - Parameters - ----------- - index: int - The index of the field to remove. - """ - try: - del self._fields[index] - except (AttributeError, IndexError): - pass - - def set_field_at(self, index, *, name, value, inline=True): - """Modifies a field to the embed object. - - The index must point to a valid pre-existing field. - - This function returns the class instance to allow for fluent-style - chaining. - - Parameters - ----------- - index: int - The index of the field to modify. - name: str - The name of the field. - value: str - The value of the field. - inline: bool - Whether the field should be displayed inline. - - Raises - ------- - IndexError - An invalid index was provided. - """ - - try: - field = self._fields[index] - except (TypeError, IndexError, AttributeError): - raise IndexError('field index out of range') - - field['name'] = str(name) - field['value'] = str(value) - field['inline'] = inline - return self - - def to_dict(self): - """Converts this embed object into a dict.""" - - # add in the raw data into the dict - result = { - key[1:]: getattr(self, key) - for key in self.__slots__ - if key[0] == '_' and hasattr(self, key) - } - - # deal with basic convenience wrappers - - try: - colour = result.pop('colour') - except KeyError: - pass - else: - if colour: - result['color'] = colour.value - - try: - timestamp = result.pop('timestamp') - except KeyError: - pass - else: - if timestamp: - result['timestamp'] = timestamp.isoformat() - - # add in the non raw attribute ones - if self.type: - result['type'] = self.type - - if self.description: - result['description'] = self.description - - if self.url: - result['url'] = self.url - - if self.title: - result['title'] = self.title - - return result diff --git a/discord.py-rewrite/discord/emoji.py b/discord.py-rewrite/discord/emoji.py deleted file mode 100644 index 6faf8db..0000000 --- a/discord.py-rewrite/discord/emoji.py +++ /dev/null @@ -1,239 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import asyncio -from collections import namedtuple - -from . import utils -from .mixins import Hashable - -class PartialReactionEmoji(namedtuple('PartialReactionEmoji', 'name id')): - """Represents a "partial" reaction emoji. - - This model will be given in two scenarios: - - - "Raw" data events such as :func:`on_raw_reaction_add` - - Custom emoji that the bot cannot see from e.g. :attr:`Message.reactions` - - .. container:: operations - - .. describe:: x == y - - Checks if two emoji are the same. - - .. describe:: x != y - - Checks if two emoji are not the same. - - .. describe:: hash(x) - - Return the emoji's hash. - - .. describe:: str(x) - - Returns the emoji rendered for discord. - - Attributes - ----------- - name: str - The custom emoji name, if applicable, or the unicode codepoint - of the non-custom emoji. - id: Optional[int] - The ID of the custom emoji, if applicable. - """ - - __slots__ = () - - def __str__(self): - if self.id is None: - return self.name - return '<:%s:%s>' % (self.name, self.id) - - def is_custom_emoji(self): - """Checks if this is a custom non-Unicode emoji.""" - return self.id is not None - - def is_unicode_emoji(self): - """Checks if this is a Unicode emoji.""" - return self.id is None - - def _as_reaction(self): - if self.id is None: - return self.name - return ':%s:%s' % (self.name, self.id) - -class Emoji(Hashable): - """Represents a custom emoji. - - Depending on the way this object was created, some of the attributes can - have a value of ``None``. - - .. container:: operations - - .. describe:: x == y - - Checks if two emoji are the same. - - .. describe:: x != y - - Checks if two emoji are not the same. - - .. describe:: hash(x) - - Return the emoji's hash. - - .. describe:: iter(x) - - Returns an iterator of ``(field, value)`` pairs. This allows this class - to be used as an iterable in list/dict/etc constructions. - - .. describe:: str(x) - - Returns the emoji rendered for discord. - - Attributes - ----------- - name: str - The name of the emoji. - id: int - The emoji's ID. - require_colons: bool - If colons are required to use this emoji in the client (:PJSalt: vs PJSalt). - managed: bool - If this emoji is managed by a Twitch integration. - guild_id: int - The guild ID the emoji belongs to. - """ - __slots__ = ('require_colons', 'managed', 'id', 'name', '_roles', 'guild_id', '_state') - - def __init__(self, *, guild, state, data): - self.guild_id = guild.id - self._state = state - self._from_data(data) - - def _from_data(self, emoji): - self.require_colons = emoji['require_colons'] - self.managed = emoji['managed'] - self.id = int(emoji['id']) - self.name = emoji['name'] - self._roles = set(emoji.get('roles', [])) - - def _iterator(self): - for attr in self.__slots__: - if attr[0] != '_': - value = getattr(self, attr, None) - if value is not None: - yield (attr, value) - - def __iter__(self): - return self._iterator() - - def __str__(self): - return "<:{0.name}:{0.id}>".format(self) - - def __repr__(self): - return ''.format(self) - - @property - def created_at(self): - """Returns the emoji's creation time in UTC.""" - return utils.snowflake_time(self.id) - - @property - def url(self): - """Returns a URL version of the emoji.""" - return "https://cdn.discordapp.com/emojis/{0.id}.png".format(self) - - @property - def roles(self): - """List[:class:`Role`]: A list of roles that is allowed to use this emoji. - - If roles is empty, the emoji is unrestricted. - """ - guild = self.guild - if guild is None: - return [] - - return [role for role in guild.roles if role.id in self._roles] - - @property - def guild(self): - """:class:`Guild`: The guild this emoji belongs to.""" - return self._state._get_guild(self.guild_id) - - @asyncio.coroutine - def delete(self, *, reason=None): - """|coro| - - Deletes the custom emoji. - - You must have :attr:`~Permissions.manage_emojis` permission to - do this. - - Guild local emotes can only be deleted by user bots. - - Parameters - ----------- - reason: Optional[str] - The reason for deleting this emoji. Shows up on the audit log. - - Raises - ------- - Forbidden - You are not allowed to delete emojis. - HTTPException - An error occurred deleting the emoji. - """ - - yield from self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason) - - @asyncio.coroutine - def edit(self, *, name, reason=None): - """|coro| - - Edits the custom emoji. - - You must have :attr:`~Permissions.manage_emojis` permission to - do this. - - Guild local emotes can only be edited by user bots. - - Parameters - ----------- - name: str - The new emoji name. - reason: Optional[str] - The reason for editing this emoji. Shows up on the audit log. - - Raises - ------- - Forbidden - You are not allowed to edit emojis. - HTTPException - An error occurred editing the emoji. - """ - - yield from self._state.http.edit_custom_emoji(self.guild.id, self.id, name=name, reason=reason) diff --git a/discord.py-rewrite/discord/enums.py b/discord.py-rewrite/discord/enums.py deleted file mode 100644 index 88857bd..0000000 --- a/discord.py-rewrite/discord/enums.py +++ /dev/null @@ -1,221 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from enum import Enum, IntEnum - -__all__ = ['ChannelType', 'MessageType', 'VoiceRegion', 'VerificationLevel', - 'ContentFilter', 'Status', 'DefaultAvatar', 'RelationshipType', - 'AuditLogAction', 'AuditLogActionCategory', 'UserFlags', ] - -class ChannelType(Enum): - text = 0 - private = 1 - voice = 2 - group = 3 - category = 4 - - def __str__(self): - return self.name - -class MessageType(Enum): - default = 0 - recipient_add = 1 - recipient_remove = 2 - call = 3 - channel_name_change = 4 - channel_icon_change = 5 - pins_add = 6 - new_member = 7 - -class VoiceRegion(Enum): - us_west = 'us-west' - us_east = 'us-east' - us_south = 'us-south' - us_central = 'us-central' - eu_west = 'eu-west' - eu_central = 'eu-central' - singapore = 'singapore' - london = 'london' - sydney = 'sydney' - amsterdam = 'amsterdam' - frankfurt = 'frankfurt' - brazil = 'brazil' - vip_us_east = 'vip-us-east' - vip_us_west = 'vip-us-west' - vip_amsterdam = 'vip-amsterdam' - - def __str__(self): - return self.value - -class VerificationLevel(IntEnum): - none = 0 - low = 1 - medium = 2 - high = 3 - table_flip = 3 - extreme = 4 - double_table_flip = 4 - - def __str__(self): - return self.name - -class ContentFilter(IntEnum): - disabled = 0 - no_role = 1 - all_members = 2 - - def __str__(self): - return self.name - -class Status(Enum): - online = 'online' - offline = 'offline' - idle = 'idle' - dnd = 'dnd' - do_not_disturb = 'dnd' - invisible = 'invisible' - - def __str__(self): - return self.value - -class DefaultAvatar(Enum): - blurple = 0 - grey = 1 - gray = 1 - green = 2 - orange = 3 - red = 4 - - def __str__(self): - return self.name - -class RelationshipType(Enum): - friend = 1 - blocked = 2 - incoming_request = 3 - outgoing_request = 4 - -class AuditLogActionCategory(Enum): - create = 1 - delete = 2 - update = 3 - -class AuditLogAction(Enum): - guild_update = 1 - channel_create = 10 - channel_update = 11 - channel_delete = 12 - overwrite_create = 13 - overwrite_update = 14 - overwrite_delete = 15 - kick = 20 - member_prune = 21 - ban = 22 - unban = 23 - member_update = 24 - member_role_update = 25 - role_create = 30 - role_update = 31 - role_delete = 32 - invite_create = 40 - invite_update = 41 - invite_delete = 42 - webhook_create = 50 - webhook_update = 51 - webhook_delete = 52 - emoji_create = 60 - emoji_update = 61 - emoji_delete = 62 - message_delete = 72 - - @property - def category(self): - lookup = { - AuditLogAction.guild_update: AuditLogActionCategory.update, - AuditLogAction.channel_create: AuditLogActionCategory.create, - AuditLogAction.channel_update: AuditLogActionCategory.update, - AuditLogAction.channel_delete: AuditLogActionCategory.delete, - AuditLogAction.overwrite_create: AuditLogActionCategory.create, - AuditLogAction.overwrite_update: AuditLogActionCategory.update, - AuditLogAction.overwrite_delete: AuditLogActionCategory.delete, - AuditLogAction.kick: None, - AuditLogAction.member_prune: None, - AuditLogAction.ban: None, - AuditLogAction.unban: None, - AuditLogAction.member_update: AuditLogActionCategory.update, - AuditLogAction.member_role_update: AuditLogActionCategory.update, - AuditLogAction.role_create: AuditLogActionCategory.create, - AuditLogAction.role_update: AuditLogActionCategory.update, - AuditLogAction.role_delete: AuditLogActionCategory.delete, - AuditLogAction.invite_create: AuditLogActionCategory.create, - AuditLogAction.invite_update: AuditLogActionCategory.update, - AuditLogAction.invite_delete: AuditLogActionCategory.delete, - AuditLogAction.webhook_create: AuditLogActionCategory.create, - AuditLogAction.webhook_update: AuditLogActionCategory.update, - AuditLogAction.webhook_delete: AuditLogActionCategory.delete, - AuditLogAction.emoji_create: AuditLogActionCategory.create, - AuditLogAction.emoji_update: AuditLogActionCategory.update, - AuditLogAction.emoji_delete: AuditLogActionCategory.delete, - AuditLogAction.message_delete: AuditLogActionCategory.delete, - } - return lookup[self] - - @property - def target_type(self): - v = self.value - if v == -1: - return 'all' - elif v < 10: - return 'guild' - elif v < 20: - return 'channel' - elif v < 30: - return 'user' - elif v < 40: - return 'role' - elif v < 50: - return 'invite' - elif v < 60: - return 'webhook' - elif v < 70: - return 'emoji' - elif v < 80: - return 'message' - -class UserFlags(Enum): - staff = 1 - partner = 2 - hypesquad = 4 - -def try_enum(cls, val): - """A function that tries to turn the value into enum ``cls``. - - If it fails it returns the value instead. - """ - try: - return cls(val) - except ValueError: - return val diff --git a/discord.py-rewrite/discord/errors.py b/discord.py-rewrite/discord/errors.py deleted file mode 100644 index 84768ca..0000000 --- a/discord.py-rewrite/discord/errors.py +++ /dev/null @@ -1,166 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -class DiscordException(Exception): - """Base exception class for discord.py - - Ideally speaking, this could be caught to handle any exceptions thrown from this library. - """ - pass - -class ClientException(DiscordException): - """Exception that's thrown when an operation in the :class:`Client` fails. - - These are usually for exceptions that happened due to user input. - """ - pass - -class NoMoreItems(DiscordException): - """Exception that is thrown when an async iteration operation has no more - items. This is mainly exposed for Python 3.4 support where `StopAsyncIteration` - is not provided. - """ - pass - -class GatewayNotFound(DiscordException): - """An exception that is usually thrown when the gateway hub - for the :class:`Client` websocket is not found.""" - def __init__(self): - message = 'The gateway to connect to discord was not found.' - super(GatewayNotFound, self).__init__(message) - -def flatten_error_dict(d, key=''): - items = [] - for k, v in d.items(): - new_key = key + '.' + k if key else k - - if isinstance(v, dict): - try: - _errors = v['_errors'] - except Exception: - items.extend(flatten_error_dict(v, new_key).items()) - else: - items.append((new_key, ' '.join(x.get('message', '') for x in _errors))) - else: - items.append((new_key, v)) - - return dict(items) - -class HTTPException(DiscordException): - """Exception that's thrown when an HTTP request operation fails. - - Attributes - ------------ - response: aiohttp.ClientResponse - The response of the failed HTTP request. This is an - instance of `aiohttp.ClientResponse`__. In some cases - this could also be a ``requests.Response``. - - __ http://aiohttp.readthedocs.org/en/stable/client_reference.html#aiohttp.ClientResponse - - text: str - The text of the error. Could be an empty string. - status: int - The status code of the HTTP request. - code: int - The Discord specific error code for the failure. - """ - - def __init__(self, response, message): - self.response = response - self.status = response.status - if isinstance(message, dict): - self.code = message.get('code', 0) - base = message.get('message', '') - errors = message.get('errors') - if errors: - errors = flatten_error_dict(errors) - helpful = '\n'.join('In %s: %s' % t for t in errors.items()) - self.text = base + '\n' + helpful - else: - self.text = base - else: - self.text = message - self.code = 0 - - fmt = '{0.reason} (status code: {0.status})' - if len(self.text): - fmt = fmt + ': {1}' - - super().__init__(fmt.format(self.response, self.text)) - -class Forbidden(HTTPException): - """Exception that's thrown for when status code 403 occurs. - - Subclass of :exc:`HTTPException` - """ - pass - -class NotFound(HTTPException): - """Exception that's thrown for when status code 404 occurs. - - Subclass of :exc:`HTTPException` - """ - pass - - -class InvalidArgument(ClientException): - """Exception that's thrown when an argument to a function - is invalid some way (e.g. wrong value or wrong type). - - This could be considered the analogous of ``ValueError`` and - ``TypeError`` except derived from :exc:`ClientException` and thus - :exc:`DiscordException`. - """ - pass - -class LoginFailure(ClientException): - """Exception that's thrown when the :meth:`Client.login` function - fails to log you in from improper credentials or some other misc. - failure. - """ - pass - -class ConnectionClosed(ClientException): - """Exception that's thrown when the gateway connection is - closed for reasons that could not be handled internally. - - Attributes - ----------- - code: int - The close code of the websocket. - reason: str - The reason provided for the closure. - shard_id: Optional[int] - The shard ID that got closed if applicable. - """ - def __init__(self, original, *, shard_id): - # This exception is just the same exception except - # reconfigured to subclass ClientException for users - self.code = original.code - self.reason = original.reason - self.shard_id = shard_id - super().__init__(str(original)) diff --git a/discord.py-rewrite/discord/ext/commands/__init__.py b/discord.py-rewrite/discord/ext/commands/__init__.py deleted file mode 100644 index f5f7bb2..0000000 --- a/discord.py-rewrite/discord/ext/commands/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -discord.ext.commands -~~~~~~~~~~~~~~~~~~~~~ - -An extension module to facilitate creation of bot commands. - -:copyright: (c) 2017 Rapptz -:license: MIT, see LICENSE for more details. -""" - -from .bot import Bot, AutoShardedBot, when_mentioned, when_mentioned_or -from .context import Context -from .core import * -from .errors import * -from .formatter import HelpFormatter, Paginator -from .converter import * -from .cooldowns import * diff --git a/discord.py-rewrite/discord/ext/commands/bot.py b/discord.py-rewrite/discord/ext/commands/bot.py deleted file mode 100644 index 048b2f4..0000000 --- a/discord.py-rewrite/discord/ext/commands/bot.py +++ /dev/null @@ -1,995 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import asyncio -import discord -import inspect -import importlib -import sys -import traceback -import re - -from .core import GroupMixin, Command, command -from .view import StringView -from .context import Context -from .errors import CommandNotFound, CommandError -from .formatter import HelpFormatter - -def when_mentioned(bot, msg): - """A callable that implements a command prefix equivalent to being mentioned. - - These are meant to be passed into the :attr:`.Bot.command_prefix` attribute. - """ - return [bot.user.mention + ' ', '<@!%s> ' % bot.user.id] - -def when_mentioned_or(*prefixes): - """A callable that implements when mentioned or other prefixes provided. - - These are meant to be passed into the :attr:`.Bot.command_prefix` attribute. - - Example - -------- - - .. code-block:: python3 - - bot = commands.Bot(command_prefix=commands.when_mentioned_or('!')) - - - .. note:: - - This callable returns another callable, so if this is done inside a custom - callable, you must call the returned callable, for example: - - .. code-block:: python3 - - async def get_prefix(bot, message): - extras = await prefixes_for(message.guild) # returns a list - return commands.when_mentioned_or(*extras)(bot, message) - - - See Also - ---------- - :func:`.when_mentioned` - """ - def inner(bot, msg): - r = list(prefixes) - r.extend(when_mentioned(bot, msg)) - return r - - return inner - -_mentions_transforms = { - '@everyone': '@\u200beveryone', - '@here': '@\u200bhere' -} - -_mention_pattern = re.compile('|'.join(_mentions_transforms.keys())) - -def _is_submodule(parent, child): - return parent == child or child.startswith(parent + ".") - -@asyncio.coroutine -def _default_help_command(ctx, *commands : str): - """Shows this message.""" - bot = ctx.bot - destination = ctx.message.author if bot.pm_help else ctx.message.channel - - def repl(obj): - return _mentions_transforms.get(obj.group(0), '') - - # help by itself just lists our own commands. - if len(commands) == 0: - pages = yield from bot.formatter.format_help_for(ctx, bot) - elif len(commands) == 1: - # try to see if it is a cog name - name = _mention_pattern.sub(repl, commands[0]) - command = None - if name in bot.cogs: - command = bot.cogs[name] - else: - command = bot.all_commands.get(name) - if command is None: - yield from destination.send(bot.command_not_found.format(name)) - return - - pages = yield from bot.formatter.format_help_for(ctx, command) - else: - name = _mention_pattern.sub(repl, commands[0]) - command = bot.all_commands.get(name) - if command is None: - yield from destination.send(bot.command_not_found.format(name)) - return - - for key in commands[1:]: - try: - key = _mention_pattern.sub(repl, key) - command = command.all_commands.get(key) - if command is None: - yield from destination.send(bot.command_not_found.format(key)) - return - except AttributeError: - yield from destination.send(bot.command_has_no_subcommands.format(command, key)) - return - - pages = yield from bot.formatter.format_help_for(ctx, command) - - if bot.pm_help is None: - characters = sum(map(lambda l: len(l), pages)) - # modify destination based on length of pages. - if characters > 1000: - destination = ctx.message.author - - for page in pages: - yield from destination.send(page) - -class BotBase(GroupMixin): - def __init__(self, command_prefix, formatter=None, description=None, pm_help=False, **options): - super().__init__(**options) - self.command_prefix = command_prefix - self.extra_events = {} - self.cogs = {} - self.extensions = {} - self._checks = [] - self._check_once = [] - self._before_invoke = None - self._after_invoke = None - self.description = inspect.cleandoc(description) if description else '' - self.pm_help = pm_help - self.owner_id = options.get('owner_id') - self.command_not_found = options.pop('command_not_found', 'No command called "{}" found.') - self.command_has_no_subcommands = options.pop('command_has_no_subcommands', 'Command {0.name} has no subcommands.') - - if options.pop('self_bot', False): - self._skip_check = lambda x, y: x != y - else: - self._skip_check = lambda x, y: x == y - - self.help_attrs = options.pop('help_attrs', {}) - - if 'name' not in self.help_attrs: - self.help_attrs['name'] = 'help' - - if formatter is not None: - if not isinstance(formatter, HelpFormatter): - raise discord.ClientException('Formatter must be a subclass of HelpFormatter') - self.formatter = formatter - else: - self.formatter = HelpFormatter() - - # pay no mind to this ugliness. - self.command(**self.help_attrs)(_default_help_command) - - # internal helpers - - def dispatch(self, event_name, *args, **kwargs): - super().dispatch(event_name, *args, **kwargs) - ev = 'on_' + event_name - for event in self.extra_events.get(ev, []): - coro = self._run_event(event, event_name, *args, **kwargs) - discord.compat.create_task(coro, loop=self.loop) - - @asyncio.coroutine - def close(self): - for extension in tuple(self.extensions): - try: - self.unload_extension(extension) - except: - pass - - for cog in tuple(self.cogs): - try: - self.remove_cog(cog) - except: - pass - - yield from super().close() - - @asyncio.coroutine - def on_command_error(self, context, exception): - """|coro| - - The default command error handler provided by the bot. - - By default this prints to ``sys.stderr`` however it could be - overridden to have a different implementation. - - This only fires if you do not specify any listeners for command error. - """ - if self.extra_events.get('on_command_error', None): - return - - if hasattr(context.command, 'on_error'): - return - - cog = context.cog - if cog: - attr = '_{0.__class__.__name__}__error'.format(cog) - if hasattr(cog, attr): - return - - print('Ignoring exception in command {}:'.format(context.command), file=sys.stderr) - traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr) - - # global check registration - - def check(self, func): - """A decorator that adds a global check to the bot. - - A global check is similar to a :func:`.check` that is applied - on a per command basis except it is run before any command checks - have been verified and applies to every command the bot has. - - .. note:: - - This function can either be a regular function or a coroutine. - - Similar to a command :func:`.check`\, this takes a single parameter - of type :class:`.Context` and can only raise exceptions derived from - :exc:`.CommandError`. - - Example - --------- - - .. code-block:: python3 - - @bot.check - def check_commands(ctx): - return ctx.command.qualified_name in allowed_commands - - """ - self.add_check(func) - return func - - def add_check(self, func, *, call_once=False): - """Adds a global check to the bot. - - This is the non-decorator interface to :meth:`.check` - and :meth:`.check_once`. - - Parameters - ----------- - func - The function that was used as a global check. - call_once: bool - If the function should only be called once per - :meth:`.Command.invoke` call. - """ - - if call_once: - self._check_once.append(func) - else: - self._checks.append(func) - - def remove_check(self, func): - """Removes a global check from the bot. - - This function is idempotent and will not raise an exception - if the function is not in the global checks. - - Parameters - ----------- - func - The function to remove from the global checks. - """ - - try: - self._checks.remove(func) - except ValueError: - try: - self._check_once.remove(func) - except ValueError: - pass - - def check_once(self, func): - """A decorator that adds a "call once" global check to the bot. - - Unlike regular global checks, this one is called only once - per :meth:`.Command.invoke` call. - - Regular global checks are called whenever a command is called - or :meth:`.Command.can_run` is called. This type of check - bypasses that and ensures that it's called only once, even inside - the default help command. - - .. note:: - - This function can either be a regular function or a coroutine. - - Similar to a command :func:`.check`\, this takes a single parameter - of type :class:`.Context` and can only raise exceptions derived from - :exc:`.CommandError`. - - Example - --------- - - .. code-block:: python3 - - @bot.check_once - def whitelist(ctx): - return ctx.message.author.id in my_whitelist - - """ - self.add_check(func, call_once=True) - return func - - @asyncio.coroutine - def can_run(self, ctx, *, call_once=False): - data = self._check_once if call_once else self._checks - - if len(data) == 0: - return True - - return (yield from discord.utils.async_all(f(ctx) for f in data)) - - @asyncio.coroutine - def is_owner(self, user): - """Checks if a :class:`.User` or :class:`.Member` is the owner of - this bot. - - If an :attr:`owner_id` is not set, it is fetched automatically - through the use of :meth:`~.Bot.application_info`. - - Parameters - ----------- - user: :class:`.abc.User` - The user to check for. - """ - - if self.owner_id is None: - app = yield from self.application_info() - self.owner_id = owner_id = app.owner.id - return user.id == owner_id - return user.id == self.owner_id - - def before_invoke(self, coro): - """A decorator that registers a coroutine as a pre-invoke hook. - - A pre-invoke hook is called directly before the command is - called. This makes it a useful function to set up database - connections or any type of set up required. - - This pre-invoke hook takes a sole parameter, a :class:`.Context`. - - .. note:: - - The :meth:`~.Bot.before_invoke` and :meth:`~.Bot.after_invoke` hooks are - only called if all checks and argument parsing procedures pass - without error. If any check or argument parsing procedures fail - then the hooks are not called. - - Parameters - ----------- - coro - The coroutine to register as the pre-invoke hook. - - Raises - ------- - :exc:`.ClientException` - The coroutine is not actually a coroutine. - """ - if not asyncio.iscoroutinefunction(coro): - raise discord.ClientException('The error handler must be a coroutine.') - - self._before_invoke = coro - return coro - - def after_invoke(self, coro): - """A decorator that registers a coroutine as a post-invoke hook. - - A post-invoke hook is called directly after the command is - called. This makes it a useful function to clean-up database - connections or any type of clean up required. - - This post-invoke hook takes a sole parameter, a :class:`.Context`. - - .. note:: - - Similar to :meth:`~.Bot.before_invoke`\, this is not called unless - checks and argument parsing procedures succeed. This hook is, - however, **always** called regardless of the internal command - callback raising an error (i.e. :exc:`.CommandInvokeError`\). - This makes it ideal for clean-up scenarios. - - Parameters - ----------- - coro - The coroutine to register as the post-invoke hook. - - Raises - ------- - :exc:`.ClientException` - The coroutine is not actually a coroutine. - """ - if not asyncio.iscoroutinefunction(coro): - raise discord.ClientException('The error handler must be a coroutine.') - - self._after_invoke = coro - return coro - - # listener registration - - def add_listener(self, func, name=None): - """The non decorator alternative to :meth:`.listen`. - - Parameters - ----------- - func : coroutine - The extra event to listen to. - name : Optional[str] - The name of the command to use. Defaults to ``func.__name__``. - - Example - -------- - - .. code-block:: python3 - - async def on_ready(): pass - async def my_message(message): pass - - bot.add_listener(on_ready) - bot.add_listener(my_message, 'on_message') - - """ - name = func.__name__ if name is None else name - - if not asyncio.iscoroutinefunction(func): - raise discord.ClientException('Listeners must be coroutines') - - if name in self.extra_events: - self.extra_events[name].append(func) - else: - self.extra_events[name] = [func] - - def remove_listener(self, func, name=None): - """Removes a listener from the pool of listeners. - - Parameters - ----------- - func - The function that was used as a listener to remove. - name - The name of the event we want to remove. Defaults to - ``func.__name__``. - """ - - name = func.__name__ if name is None else name - - if name in self.extra_events: - try: - self.extra_events[name].remove(func) - except ValueError: - pass - - def listen(self, name=None): - """A decorator that registers another function as an external - event listener. Basically this allows you to listen to multiple - events from different places e.g. such as :func:`.on_ready` - - The functions being listened to must be a coroutine. - - Example - -------- - - .. code-block:: python3 - - @bot.listen() - async def on_message(message): - print('one') - - # in some other file... - - @bot.listen('on_message') - async def my_message(message): - print('two') - - Would print one and two in an unspecified order. - - Raises - ------- - :exc:`.ClientException` - The function being listened to is not a coroutine. - """ - - def decorator(func): - self.add_listener(func, name) - return func - - return decorator - - # cogs - - def add_cog(self, cog): - """Adds a "cog" to the bot. - - A cog is a class that has its own event listeners and commands. - - They are meant as a way to organize multiple relevant commands - into a singular class that shares some state or no state at all. - - The cog can also have a ``__global_check`` member function that allows - you to define a global check. See :meth:`.check` for more info. If - the name is ``__global_check_once`` then it's equivalent to the - :meth:`.check_once` decorator. - - More information will be documented soon. - - Parameters - ----------- - cog - The cog to register to the bot. - """ - - self.cogs[type(cog).__name__] = cog - - try: - check = getattr(cog, '_{.__class__.__name__}__global_check'.format(cog)) - except AttributeError: - pass - else: - self.add_check(check) - - try: - check = getattr(cog, '_{.__class__.__name__}__global_check_once'.format(cog)) - except AttributeError: - pass - else: - self.add_check(check, call_once=True) - - members = inspect.getmembers(cog) - for name, member in members: - # register commands the cog has - if isinstance(member, Command): - if member.parent is None: - self.add_command(member) - continue - - # register event listeners the cog has - if name.startswith('on_'): - self.add_listener(member, name) - - def get_cog(self, name): - """Gets the cog instance requested. - - If the cog is not found, ``None`` is returned instead. - - Parameters - ----------- - name : str - The name of the cog you are requesting. - """ - return self.cogs.get(name) - - def get_cog_commands(self, name): - """Gets a unique set of the cog's registered commands - without aliases. - - If the cog is not found, an empty set is returned. - - Parameters - ------------ - name: str - The name of the cog whose commands you are requesting. - - Returns - --------- - Set[:class:`.Command`] - A unique set of commands without aliases that belong - to the cog. - """ - - try: - cog = self.cogs[name] - except KeyError: - return set() - - return {c for c in self.all_commands.values() if c.instance is cog} - - def remove_cog(self, name): - """Removes a cog from the bot. - - All registered commands and event listeners that the - cog has registered will be removed as well. - - If no cog is found then this method has no effect. - - If the cog defines a special member function named ``__unload`` - then it is called when removal has completed. This function - **cannot** be a coroutine. It must be a regular function. - - Parameters - ----------- - name : str - The name of the cog to remove. - """ - - cog = self.cogs.pop(name, None) - if cog is None: - return - - members = inspect.getmembers(cog) - for name, member in members: - # remove commands the cog has - if isinstance(member, Command): - if member.parent is None: - self.remove_command(member.name) - continue - - # remove event listeners the cog has - if name.startswith('on_'): - self.remove_listener(member) - - try: - check = getattr(cog, '_{0.__class__.__name__}__global_check'.format(cog)) - except AttributeError: - pass - else: - self.remove_check(check) - - try: - check = getattr(cog, '_{0.__class__.__name__}__global_check_once'.format(cog)) - except AttributeError: - pass - else: - self.remove_check(check) - - unloader_name = '_{0.__class__.__name__}__unload'.format(cog) - try: - unloader = getattr(cog, unloader_name) - except AttributeError: - pass - else: - unloader() - - del cog - - # extensions - - def load_extension(self, name): - """Loads an extension. - - An extension is a python module that contains commands, cogs, or - listeners. - - An extension must have a global function, ``setup`` defined as - the entry point on what to do when the extension is loaded. This entry - point must have a single argument, the ``bot``. - - Parameters - ------------ - name: str - The extension name to load. It must be dot separated like - regular Python imports if accessing a sub-module. e.g. - ``foo.test`` if you want to import ``foo/test.py``. - - Raises - -------- - ClientException - The extension does not have a setup function. - ImportError - The extension could not be imported. - """ - - if name in self.extensions: - return - - lib = importlib.import_module(name) - if not hasattr(lib, 'setup'): - del lib - del sys.modules[name] - raise discord.ClientException('extension does not have a setup function') - - lib.setup(self) - self.extensions[name] = lib - - def unload_extension(self, name): - """Unloads an extension. - - When the extension is unloaded, all commands, listeners, and cogs are - removed from the bot and the module is un-imported. - - The extension can provide an optional global function, ``teardown``, - to do miscellaneous clean-up if necessary. This function takes a single - parameter, the ``bot``, similar to ``setup`` from - :func:`~.Bot.load_extension`. - - Parameters - ------------ - name: str - The extension name to unload. It must be dot separated like - regular Python imports if accessing a sub-module. e.g. - ``foo.test`` if you want to import ``foo/test.py``. - """ - - lib = self.extensions.get(name) - if lib is None: - return - - lib_name = lib.__name__ - - # find all references to the module - - # remove the cogs registered from the module - for cogname, cog in self.cogs.copy().items(): - if _is_submodule(lib_name, cog.__module__): - self.remove_cog(cogname) - - # first remove all the commands from the module - for cmd in self.all_commands.copy().values(): - if _is_submodule(lib_name, cmd.module): - if isinstance(cmd, GroupMixin): - cmd.recursively_remove_all_commands() - self.remove_command(cmd.name) - - # then remove all the listeners from the module - for event_list in self.extra_events.copy().values(): - remove = [] - for index, event in enumerate(event_list): - if _is_submodule(lib_name, event.__module__): - remove.append(index) - - for index in reversed(remove): - del event_list[index] - - try: - func = getattr(lib, 'teardown') - except AttributeError: - pass - else: - try: - func(self) - except: - pass - finally: - # finally remove the import.. - del lib - del self.extensions[name] - del sys.modules[name] - for module in list(sys.modules.keys()): - if _is_submodule(lib_name, module): - del sys.modules[module] - - # command processing - - @asyncio.coroutine - def get_prefix(self, message): - """|coro| - - Retrieves the prefix the bot is listening to - with the message as a context. - - Parameters - ----------- - message: :class:`discord.Message` - The message context to get the prefix of. - - Raises - -------- - :exc:`.ClientException` - The prefix was invalid. This could be if the prefix - function returned None, the prefix list returned no - elements that aren't None, or the prefix string is - empty. - - Returns - -------- - Union[List[str], str] - A list of prefixes or a single prefix that the bot is - listening for. - """ - prefix = ret = self.command_prefix - if callable(prefix): - ret = prefix(self, message) - if asyncio.iscoroutine(ret): - ret = yield from ret - - if isinstance(ret, (list, tuple)): - ret = [p for p in ret if p] - - if not ret: - raise discord.ClientException('invalid prefix (could be an empty string, empty list, or None)') - - return ret - - @asyncio.coroutine - def get_context(self, message, *, cls=Context): - """|coro| - - Returns the invocation context from the message. - - This is a more low-level counter-part for :meth:`.process_commands` - to allow users more fine grained control over the processing. - - The returned context is not guaranteed to be a valid invocation - context, :attr:`.Context.valid` must be checked to make sure it is. - If the context is not valid then it is not a valid candidate to be - invoked under :meth:`~.Bot.invoke`. - - Parameters - ----------- - message: :class:`discord.Message` - The message to get the invocation context from. - cls - The factory class that will be used to create the context. - By default, this is :class:`.Context`. Should a custom - class be provided, it must be similar enough to :class:`.Context`\'s - interface. - - Returns - -------- - :class:`.Context` - The invocation context. The type of this can change via the - ``cls`` parameter. - """ - - view = StringView(message.content) - ctx = cls(prefix=None, view=view, bot=self, message=message) - - if self._skip_check(message.author.id, self.user.id): - return ctx - - prefix = yield from self.get_prefix(message) - invoked_prefix = prefix - - if isinstance(prefix, str): - if not view.skip_string(prefix): - return ctx - else: - invoked_prefix = discord.utils.find(view.skip_string, prefix) - if invoked_prefix is None: - return ctx - - invoker = view.get_word() - ctx.invoked_with = invoker - ctx.prefix = invoked_prefix - ctx.command = self.all_commands.get(invoker) - return ctx - - @asyncio.coroutine - def invoke(self, ctx): - """|coro| - - Invokes the command given under the invocation context and - handles all the internal event dispatch mechanisms. - - Parameters - ----------- - ctx: :class:`.Context` - The invocation context to invoke. - """ - if ctx.command is not None: - self.dispatch('command', ctx) - try: - if (yield from self.can_run(ctx, call_once=True)): - yield from ctx.command.invoke(ctx) - except CommandError as e: - yield from ctx.command.dispatch_error(ctx, e) - else: - self.dispatch('command_completion', ctx) - elif ctx.invoked_with: - exc = CommandNotFound('Command "{}" is not found'.format(ctx.invoked_with)) - self.dispatch('command_error', ctx, exc) - - @asyncio.coroutine - def process_commands(self, message): - """|coro| - - This function processes the commands that have been registered - to the bot and other groups. Without this coroutine, none of the - commands will be triggered. - - By default, this coroutine is called inside the :func:`.on_message` - event. If you choose to override the :func:`.on_message` event, then - you should invoke this coroutine as well. - - This is built using other low level tools, and is equivalent to a - call to :meth:`~.Bot.get_context` followed by a call to :meth:`~.Bot.invoke`. - - Parameters - ----------- - message : discord.Message - The message to process commands for. - """ - ctx = yield from self.get_context(message) - yield from self.invoke(ctx) - - @asyncio.coroutine - def on_message(self, message): - yield from self.process_commands(message) - -class Bot(BotBase, discord.Client): - """Represents a discord bot. - - This class is a subclass of :class:`discord.Client` and as a result - anything that you can do with a :class:`discord.Client` you can do with - this bot. - - .. _deque: https://docs.python.org/3.4/library/collections.html#collections.deque - .. _event loop: https://docs.python.org/3/library/asyncio-eventloops.html - - This class also subclasses :class:`.GroupMixin` to provide the functionality - to manage commands. - - Attributes - ----------- - command_prefix - The command prefix is what the message content must contain initially - to have a command invoked. This prefix could either be a string to - indicate what the prefix should be, or a callable that takes in the bot - as its first parameter and :class:`discord.Message` as its second - parameter and returns the prefix. This is to facilitate "dynamic" - command prefixes. This callable can be either a regular function or - a coroutine. - - The command prefix could also be a list or a tuple indicating that - multiple checks for the prefix should be used and the first one to - match will be the invocation prefix. You can get this prefix via - :attr:`.Context.prefix`. - description : str - The content prefixed into the default help message. - self_bot : bool - If ``True``, the bot will only listen to commands invoked by itself rather - than ignoring itself. If ``False`` (the default) then the bot will ignore - itself. This cannot be changed once initialised. - formatter : :class:`.HelpFormatter` - The formatter used to format the help message. By default, it uses a - the :class:`.HelpFormatter`. Check it for more info on how to override it. - If you want to change the help command completely (add aliases, etc) then - a call to :meth:`~.Bot.remove_command` with 'help' as the argument would do the - trick. - pm_help : Optional[bool] - A tribool that indicates if the help command should PM the user instead of - sending it to the channel it received it from. If the boolean is set to - ``True``, then all help output is PM'd. If ``False``, none of the help - output is PM'd. If ``None``, then the bot will only PM when the help - message becomes too long (dictated by more than 1000 characters). - Defaults to ``False``. - help_attrs : dict - A dictionary of options to pass in for the construction of the help command. - This allows you to change the command behaviour without actually changing - the implementation of the command. The attributes will be the same as the - ones passed in the :class:`.Command` constructor. Note that ``pass_context`` - will always be set to ``True`` regardless of what you pass in. - command_not_found : str - The format string used when the help command is invoked with a command that - is not found. Useful for i18n. Defaults to ``"No command called {} found."``. - The only format argument is the name of the command passed. - command_has_no_subcommands : str - The format string used when the help command is invoked with requests for a - subcommand but the command does not have any subcommands. Defaults to - ``"Command {0.name} has no subcommands."``. The first format argument is the - :class:`.Command` attempted to get a subcommand and the second is the name. - owner_id: Optional[int] - The ID that owns the bot. If this is not set and is then queried via - :meth:`.is_owner` then it is fetched automatically using - :meth:`~.Bot.application_info`. - """ - pass - -class AutoShardedBot(BotBase, discord.AutoShardedClient): - """This is similar to :class:`.Bot` except that it is derived from - :class:`discord.AutoShardedClient` instead. - """ - pass diff --git a/discord.py-rewrite/discord/ext/commands/context.py b/discord.py-rewrite/discord/ext/commands/context.py deleted file mode 100644 index c250fc6..0000000 --- a/discord.py-rewrite/discord/ext/commands/context.py +++ /dev/null @@ -1,227 +0,0 @@ -# -*- coding: utf-8 -*- -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import asyncio -import discord.abc -import discord.utils - -class Context(discord.abc.Messageable): - """Represents the context in which a command is being invoked under. - - This class contains a lot of meta data to help you understand more about - the invocation context. This class is not created manually and is instead - passed around to commands as the first parameter. - - This class implements the :class:`abc.Messageable` ABC. - - Attributes - ----------- - message: :class:`discord.Message` - The message that triggered the command being executed. - bot: :class:`.Bot` - The bot that contains the command being executed. - args: list - The list of transformed arguments that were passed into the command. - If this is accessed during the :func:`on_command_error` event - then this list could be incomplete. - kwargs: dict - A dictionary of transformed arguments that were passed into the command. - Similar to :attr:`args`\, if this is accessed in the - :func:`on_command_error` event then this dict could be incomplete. - prefix: str - The prefix that was used to invoke the command. - command - The command (i.e. :class:`.Command` or its superclasses) that is being - invoked currently. - invoked_with: str - The command name that triggered this invocation. Useful for finding out - which alias called the command. - invoked_subcommand - The subcommand (i.e. :class:`.Command` or its superclasses) that was - invoked. If no valid subcommand was invoked then this is equal to - `None`. - subcommand_passed: Optional[str] - The string that was attempted to call a subcommand. This does not have - to point to a valid registered subcommand and could just point to a - nonsense string. If nothing was passed to attempt a call to a - subcommand then this is set to `None`. - command_failed: bool - A boolean that indicates if the command failed to be parsed, checked, - or invoked. - """ - - def __init__(self, **attrs): - self.message = attrs.pop('message', None) - self.bot = attrs.pop('bot', None) - self.args = attrs.pop('args', []) - self.kwargs = attrs.pop('kwargs', {}) - self.prefix = attrs.pop('prefix') - self.command = attrs.pop('command', None) - self.view = attrs.pop('view', None) - self.invoked_with = attrs.pop('invoked_with', None) - self.invoked_subcommand = attrs.pop('invoked_subcommand', None) - self.subcommand_passed = attrs.pop('subcommand_passed', None) - self.command_failed = attrs.pop('command_failed', False) - self._state = self.message._state - - @asyncio.coroutine - def invoke(self, *args, **kwargs): - """|coro| - - Calls a command with the arguments given. - - This is useful if you want to just call the callback that a - :class:`.Command` holds internally. - - Note - ------ - You do not pass in the context as it is done for you. - - Warning - --------- - The first parameter passed **must** be the command being invoked. - - Parameters - ----------- - command: :class:`.Command` - A command or superclass of a command that is going to be called. - \*args - The arguments to to use. - \*\*kwargs - The keyword arguments to use. - """ - - try: - command = args[0] - except IndexError: - raise TypeError('Missing command to invoke.') from None - - arguments = [] - if command.instance is not None: - arguments.append(command.instance) - - arguments.append(self) - arguments.extend(args[1:]) - - ret = yield from command.callback(*arguments, **kwargs) - return ret - - @asyncio.coroutine - def reinvoke(self, *, call_hooks=False, restart=True): - """|coro| - - Calls the command again. - - This is similar to :meth:`~.Context.invoke` except that it bypasses - checks, cooldowns, and error handlers. - - .. note:: - - If you want to bypass :exc:`.UserInputError` derived exceptions, - it is recommended to use the regular :meth:`~.Context.invoke` - as it will work more naturally. After all, this will end up - using the old arguments the user has used and will thus just - fail again. - - Parameters - ------------ - call_hooks: bool - Whether to call the before and after invoke hooks. - restart: bool - Whether to start the call chain from the very beginning - or where we left off (i.e. the command that caused the error). - The default is to start where we left off. - """ - cmd = self.command - view = self.view - if cmd is None: - raise ValueError('This context is not valid.') - - # some state to revert to when we're done - index, previous = view.index, view.previous - invoked_with = self.invoked_with - invoked_subcommand = self.invoked_subcommand - subcommand_passed = self.subcommand_passed - - if restart: - to_call = cmd.root_parent or cmd - view.index = len(self.prefix) - view.previous = 0 - view.get_word() # advance to get the root command - else: - to_call = cmd - - try: - yield from to_call.reinvoke(self, call_hooks=call_hooks) - finally: - self.command = cmd - view.index = index - view.previous = previous - self.invoked_with = invoked_with - self.invoked_subcommand = invoked_subcommand - self.subcommand_passed = subcommand_passed - - @property - def valid(self): - """Checks if the invocation context is valid to be invoked with.""" - return self.prefix is not None and self.command is not None - - @asyncio.coroutine - def _get_channel(self): - return self.channel - - @property - def cog(self): - """Returns the cog associated with this context's command. None if it does not exist.""" - - if self.command is None: - return None - return self.command.instance - - @discord.utils.cached_property - def guild(self): - """Returns the guild associated with this context's command. None if not available.""" - return self.message.guild - - @discord.utils.cached_property - def channel(self): - """Returns the channel associated with this context's command. Shorthand for :attr:`Message.channel`.""" - return self.message.channel - - @discord.utils.cached_property - def author(self): - """Returns the author associated with this context's command. Shorthand for :attr:`Message.author`""" - return self.message.author - - @discord.utils.cached_property - def me(self): - """Similar to :attr:`Guild.me` except it may return the :class:`ClientUser` in private message contexts.""" - return self.guild.me if self.guild is not None else self.bot.user - - @property - def voice_client(self): - """Optional[:class:`VoiceClient`]: A shortcut to :attr:`Guild.voice_client`\, if applicable.""" - g = self.guild - return g.voice_client if g else None diff --git a/discord.py-rewrite/discord/ext/commands/converter.py b/discord.py-rewrite/discord/ext/commands/converter.py deleted file mode 100644 index 47ce8cc..0000000 --- a/discord.py-rewrite/discord/ext/commands/converter.py +++ /dev/null @@ -1,481 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import discord -import asyncio -import re -import inspect - -from .errors import BadArgument, NoPrivateMessage -from .view import StringView - -__all__ = [ 'Converter', 'MemberConverter', 'UserConverter', - 'TextChannelConverter', 'InviteConverter', 'RoleConverter', - 'GameConverter', 'ColourConverter', 'VoiceChannelConverter', - 'EmojiConverter','CategoryChannelConverter', 'IDConverter', - 'clean_content' ] - -def _get_from_guilds(bot, getter, argument): - result = None - for guild in bot.guilds: - result = getattr(guild, getter)(argument) - if result: - return result - return result - -class Converter: - """The base class of custom converters that require the :class:`.Context` - to be passed to be useful. - - This allows you to implement converters that function similar to the - special cased ``discord`` classes. - - Classes that derive from this should override the :meth:`~.Converter.convert` - method to do its conversion logic. This method must be a coroutine. - """ - - @asyncio.coroutine - def convert(self, ctx, argument): - """|coro| - - The method to override to do conversion logic. - - If an error is found while converting, it is recommended to - raise a :exc:`.CommandError` derived exception as it will - properly propagate to the error handlers. - - Parameters - ----------- - ctx: :class:`.Context` - The invocation context that the argument is being used in. - argument: str - The argument that is being converted. - """ - raise NotImplementedError('Derived classes need to implement this.') - -class IDConverter(Converter): - def __init__(self): - self._id_regex = re.compile(r'([0-9]{15,21})$') - super().__init__() - - def _get_id_match(self, argument): - return self._id_regex.match(argument) - -class MemberConverter(IDConverter): - """Converts to a :class:`Member`. - - All lookups are via the local guild. If in a DM context, then the lookup - is done by the global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name#discrim - 4. Lookup by name - 5. Lookup by nickname - """ - - @asyncio.coroutine - def convert(self, ctx, argument): - message = ctx.message - bot = ctx.bot - match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument) - guild = message.guild - result = None - if match is None: - # not a mention... - if guild: - result = guild.get_member_named(argument) - else: - result = _get_from_guilds(bot, 'get_member_named', argument) - else: - user_id = int(match.group(1)) - if guild: - result = guild.get_member(user_id) - else: - result = _get_from_guilds(bot, 'get_member', user_id) - - if result is None: - raise BadArgument('Member "{}" not found'.format(argument)) - - return result - -class UserConverter(IDConverter): - """Converts to a :class:`User`. - - All lookups are via the global user cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name#discrim - 4. Lookup by name - """ - @asyncio.coroutine - def convert(self, ctx, argument): - match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument) - result = None - state = ctx._state - - if match is not None: - user_id = int(match.group(1)) - result = ctx.bot.get_user(user_id) - else: - arg = argument - # check for discriminator if it exists - if len(arg) > 5 and arg[-5] == '#': - discrim = arg[-4:] - name = arg[:-5] - predicate = lambda u: u.name == name and u.discriminator == discrim - result = discord.utils.find(predicate, state._users.values()) - if result is not None: - return result - - predicate = lambda u: u.name == arg - result = discord.utils.find(predicate, state._users.values()) - - if result is None: - raise BadArgument('User "{}" not found'.format(argument)) - - return result - -class TextChannelConverter(IDConverter): - """Converts to a :class:`TextChannel`. - - All lookups are via the local guild. If in a DM context, then the lookup - is done by the global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name - """ - @asyncio.coroutine - def convert(self, ctx, argument): - bot = ctx.bot - - match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument) - result = None - guild = ctx.guild - - if match is None: - # not a mention - if guild: - result = discord.utils.get(guild.text_channels, name=argument) - else: - def check(c): - return isinstance(c, discord.TextChannel) and c.name == argument - result = discord.utils.find(check, bot.get_all_channels()) - else: - channel_id = int(match.group(1)) - if guild: - result = guild.get_channel(channel_id) - else: - result = _get_from_guilds(bot, 'get_channel', channel_id) - - if not isinstance(result, discord.TextChannel): - raise BadArgument('Channel "{}" not found.'.format(argument)) - - return result - -class VoiceChannelConverter(IDConverter): - """Converts to a :class:`VoiceChannel`. - - All lookups are via the local guild. If in a DM context, then the lookup - is done by the global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name - """ - @asyncio.coroutine - def convert(self, ctx, argument): - bot = ctx.bot - match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument) - result = None - guild = ctx.guild - - if match is None: - # not a mention - if guild: - result = discord.utils.get(guild.voice_channels, name=argument) - else: - def check(c): - return isinstance(c, discord.VoiceChannel) and c.name == argument - result = discord.utils.find(check, bot.get_all_channels()) - else: - channel_id = int(match.group(1)) - if guild: - result = guild.get_channel(channel_id) - else: - result = _get_from_guilds(bot, 'get_channel', channel_id) - - if not isinstance(result, discord.VoiceChannel): - raise BadArgument('Channel "{}" not found.'.format(argument)) - - return result - -class CategoryChannelConverter(IDConverter): - """Converts to a :class:`CategoryChannel`. - - All lookups are via the local guild. If in a DM context, then the lookup - is done by the global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name - """ - @asyncio.coroutine - def convert(self, ctx, argument): - bot = ctx.bot - - match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument) - result = None - guild = ctx.guild - - if match is None: - # not a mention - if guild: - result = discord.utils.get(guild.categories, name=argument) - else: - def check(c): - return isinstance(c, discord.CategoryChannel) and c.name == argument - result = discord.utils.find(check, bot.get_all_channels()) - else: - channel_id = int(match.group(1)) - if guild: - result = guild.get_channel(channel_id) - else: - result = _get_from_guilds(bot, 'get_channel', channel_id) - - if not isinstance(result, discord.CategoryChannel): - raise BadArgument('Channel "{}" not found.'.format(argument)) - - return result - -class ColourConverter(Converter): - """Converts to a :class:`Colour`. - - The following formats are accepted: - - - ``0x`` - - ``#`` - - ``0x#`` - - Any of the ``classmethod`` in :class:`Colour` - - - The ``_`` in the name can be optionally replaced with spaces. - """ - @asyncio.coroutine - def convert(self, ctx, argument): - arg = argument.replace('0x', '').lower() - - if arg[0] == '#': - arg = arg[1:] - try: - value = int(arg, base=16) - return discord.Colour(value=value) - except ValueError: - method = getattr(discord.Colour, arg.replace(' ', '_'), None) - if method is None or not inspect.ismethod(method): - raise BadArgument('Colour "{}" is invalid.'.format(arg)) - return method() - -class RoleConverter(IDConverter): - """Converts to a :class:`Role`. - - - All lookups are via the local guild. If in a DM context, then the lookup - is done by the global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name - """ - @asyncio.coroutine - def convert(self, ctx, argument): - guild = ctx.message.guild - if not guild: - raise NoPrivateMessage() - - match = self._get_id_match(argument) or re.match(r'<@&([0-9]+)>$', argument) - params = dict(id=int(match.group(1))) if match else dict(name=argument) - result = discord.utils.get(guild.roles, **params) - if result is None: - raise BadArgument('Role "{}" not found.'.format(argument)) - return result - -class GameConverter(Converter): - """Converts to :class:`Game`.""" - @asyncio.coroutine - def convert(self, ctx, argument): - return discord.Game(name=argument) - -class InviteConverter(Converter): - """Converts to a :class:`Invite`. - - This is done via an HTTP request using :meth:`.Bot.get_invite`. - """ - @asyncio.coroutine - def convert(self, ctx, argument): - try: - invite = yield from ctx.bot.get_invite(argument) - return invite - except Exception as e: - raise BadArgument('Invite is invalid or expired') from e - -class EmojiConverter(IDConverter): - """Converts to a :class:`Emoji`. - - - All lookups are done for the local guild first, if available. If that lookup - fails, then it checks the client's global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by extracting ID from the emoji. - 3. Lookup by name - """ - @asyncio.coroutine - def convert(self, ctx, argument): - match = self._get_id_match(argument) or re.match(r'<:[a-zA-Z0-9\_]+:([0-9]+)>$', argument) - result = None - bot = ctx.bot - guild = ctx.guild - - if match is None: - # Try to get the emoji by name. Try local guild first. - if guild: - result = discord.utils.get(guild.emojis, name=argument) - - if result is None: - result = discord.utils.get(bot.emojis, name=argument) - else: - emoji_id = int(match.group(1)) - - # Try to look up emoji by id. - if guild: - result = discord.utils.get(guild.emojis, id=emoji_id) - - if result is None: - result = discord.utils.get(bot.emojis, id=emoji_id) - - if result is None: - raise BadArgument('Emoji "{}" not found.'.format(argument)) - - return result - -class clean_content(Converter): - """Converts the argument to mention scrubbed version of - said content. - - This behaves similarly to :attr:`.Message.clean_content`. - - Attributes - ------------ - fix_channel_mentions: bool - Whether to clean channel mentions. - use_nicknames: bool - Whether to use nicknames when transforming mentions. - escape_markdown: bool - Whether to also escape special markdown characters. - """ - def __init__(self, *, fix_channel_mentions=False, use_nicknames=True, escape_markdown=False): - self.fix_channel_mentions = fix_channel_mentions - self.use_nicknames = use_nicknames - self.escape_markdown = escape_markdown - - @asyncio.coroutine - def convert(self, ctx, argument): - message = ctx.message - transformations = {} - - if self.fix_channel_mentions and ctx.guild: - def resolve_channel(id, *, _get=ctx.guild.get_channel): - ch = _get(id) - return ('<#%s>' % id), ('#' + ch.name if ch else '#deleted-channel') - - transformations.update(resolve_channel(channel) for channel in message.raw_channel_mentions) - - if self.use_nicknames and ctx.guild: - def resolve_member(id, *, _get=ctx.guild.get_member): - m = _get(id) - return '@' + m.display_name if m else '@deleted-user' - else: - def resolve_member(id, *, _get=ctx.bot.get_user): - m = _get(id) - return '@' + m.name if m else '@deleted-user' - - - transformations.update( - ('<@%s>' % member_id, resolve_member(member_id)) - for member_id in message.raw_mentions - ) - - transformations.update( - ('<@!%s>' % member_id, resolve_member(member_id)) - for member_id in message.raw_mentions - ) - - if ctx.guild: - def resolve_role(id, *, _find=discord.utils.find, _roles=ctx.guild.roles): - r = _find(lambda x: x.id == id, _roles) - return '@' + r.name if r else '@deleted-role' - - transformations.update( - ('<@&%s>' % role_id, resolve_role(role_id)) - for role_id in message.raw_role_mentions - ) - - def repl(obj): - return transformations.get(obj.group(0), '') - - pattern = re.compile('|'.join(transformations.keys())) - result = pattern.sub(repl, argument) - - if self.escape_markdown: - transformations = { - re.escape(c): '\\' + c - for c in ('*', '`', '_', '~', '\\') - } - - def replace(obj): - return transformations.get(re.escape(obj.group(0)), '') - - pattern = re.compile('|'.join(transformations.keys())) - result = pattern.sub(replace, result) - - # Completely ensure no mentions escape: - return re.sub(r'@(everyone|here|[!&]?[0-9]{17,21})', '@\u200b\\1', result) diff --git a/discord.py-rewrite/discord/ext/commands/cooldowns.py b/discord.py-rewrite/discord/ext/commands/cooldowns.py deleted file mode 100644 index 7414628..0000000 --- a/discord.py-rewrite/discord/ext/commands/cooldowns.py +++ /dev/null @@ -1,130 +0,0 @@ -# -*- coding: utf-8 -*- -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import enum -import time - -__all__ = ['BucketType', 'Cooldown', 'CooldownMapping'] - -class BucketType(enum.Enum): - default = 0 - user = 1 - guild = 2 - channel = 3 - -class Cooldown: - __slots__ = ('rate', 'per', 'type', '_window', '_tokens', '_last') - - def __init__(self, rate, per, type): - self.rate = int(rate) - self.per = float(per) - self.type = type - self._window = 0.0 - self._tokens = self.rate - self._last = 0.0 - - if not isinstance(self.type, BucketType): - raise TypeError('Cooldown type must be a BucketType') - - def is_rate_limited(self): - current = time.time() - self._last = current - - # first token used means that we start a new rate limit window - if self._tokens == self.rate: - self._window = current - - # check if our window has passed and we can refresh our tokens - if current > self._window + self.per: - self._tokens = self.rate - self._window = current - - # check if we're rate limited - if self._tokens == 0: - return self.per - (current - self._window) - - # we're not so decrement our tokens - self._tokens -= 1 - - # see if we got rate limited due to this token change, and if - # so update the window to point to our current time frame - if self._tokens == 0: - self._window = current - - def reset(self): - self._tokens = self.rate - self._last = 0.0 - - def copy(self): - return Cooldown(self.rate, self.per, self.type) - - def __repr__(self): - return ''.format(self) - -class CooldownMapping: - def __init__(self, original): - self._cache = {} - self._cooldown = original - - @property - def valid(self): - return self._cooldown is not None - - @classmethod - def from_cooldown(cls, rate, per, type): - return cls(Cooldown(rate, per, type)) - - def _bucket_key(self, ctx): - msg = ctx.message - bucket_type = self._cooldown.type - if bucket_type is BucketType.user: - return msg.author.id - elif bucket_type is BucketType.guild: - return getattr(msg.guild, 'id', msg.author.id) - elif bucket_type is BucketType.channel: - return msg.channel.id - - def _verify_cache_integrity(self): - # we want to delete all cache objects that haven't been used - # in a cooldown window. e.g. if we have a command that has a - # cooldown of 60s and it has not been used in 60s then that key should be deleted - current = time.time() - dead_keys = [k for k, v in self._cache.items() if current > v._last + v.per] - for k in dead_keys: - del self._cache[k] - - def get_bucket(self, ctx): - if self._cooldown.type is BucketType.default: - return self._cooldown - - self._verify_cache_integrity() - key = self._bucket_key(ctx) - if key not in self._cache: - bucket = self._cooldown.copy() - self._cache[key] = bucket - else: - bucket = self._cache[key] - - return bucket diff --git a/discord.py-rewrite/discord/ext/commands/core.py b/discord.py-rewrite/discord/ext/commands/core.py deleted file mode 100644 index 070e7e3..0000000 --- a/discord.py-rewrite/discord/ext/commands/core.py +++ /dev/null @@ -1,1275 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import asyncio -import inspect -import discord -import functools - -from .errors import * -from .cooldowns import Cooldown, BucketType, CooldownMapping -from .view import quoted_word -from . import converter as converters - -__all__ = [ 'Command', 'Group', 'GroupMixin', 'command', 'group', - 'has_role', 'has_permissions', 'has_any_role', 'check', - 'bot_has_role', 'bot_has_permissions', 'bot_has_any_role', - 'cooldown', 'guild_only', 'is_owner', 'is_nsfw', ] - -def wrap_callback(coro): - @functools.wraps(coro) - @asyncio.coroutine - def wrapped(*args, **kwargs): - try: - ret = yield from coro(*args, **kwargs) - except CommandError: - raise - except asyncio.CancelledError: - return - except Exception as e: - raise CommandInvokeError(e) from e - return ret - return wrapped - -def hooked_wrapped_callback(command, ctx, coro): - @functools.wraps(coro) - @asyncio.coroutine - def wrapped(*args, **kwargs): - try: - ret = yield from coro(*args, **kwargs) - except CommandError: - ctx.command_failed = True - raise - except asyncio.CancelledError: - ctx.command_failed = True - return - except Exception as e: - ctx.command_failed = True - raise CommandInvokeError(e) from e - finally: - yield from command.call_after_hooks(ctx) - return ret - return wrapped - -def _convert_to_bool(argument): - lowered = argument.lower() - if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'): - return True - elif lowered in ('no', 'n', 'false', 'f', '0', 'disable', 'off'): - return False - else: - raise BadArgument(lowered + ' is not a recognised boolean option') - -class Command: - """A class that implements the protocol for a bot text command. - - These are not created manually, instead they are created via the - decorator or functional interface. - - Attributes - ----------- - name: str - The name of the command. - callback: coroutine - The coroutine that is executed when the command is called. - help: str - The long help text for the command. - brief: str - The short help text for the command. If this is not specified - then the first line of the long help text is used instead. - usage: str - A replacement for arguments in the default help text. - aliases: list - The list of aliases the command can be invoked under. - enabled: bool - A boolean that indicates if the command is currently enabled. - If the command is invoked while it is disabled, then - :exc:`.DisabledCommand` is raised to the :func:`.on_command_error` - event. Defaults to ``True``. - parent: Optional[command] - The parent command that this command belongs to. ``None`` is there - isn't one. - checks - A list of predicates that verifies if the command could be executed - with the given :class:`.Context` as the sole parameter. If an exception - is necessary to be thrown to signal failure, then one derived from - :exc:`.CommandError` should be used. Note that if the checks fail then - :exc:`.CheckFailure` exception is raised to the :func:`.on_command_error` - event. - description: str - The message prefixed into the default help command. - hidden: bool - If ``True``\, the default help command does not show this in the - help output. - rest_is_raw: bool - If ``False`` and a keyword-only argument is provided then the keyword - only argument is stripped and handled as if it was a regular argument - that handles :exc:`.MissingRequiredArgument` and default values in a - regular matter rather than passing the rest completely raw. If ``True`` - then the keyword-only argument will pass in the rest of the arguments - in a completely raw matter. Defaults to ``False``. - ignore_extra: bool - If ``True``\, ignores extraneous strings passed to a command if all its - requirements are met (e.g. ``?foo a b c`` when only expecting ``a`` - and ``b``). Otherwise :func:`.on_command_error` and local error handlers - are called with :exc:`.TooManyArguments`. Defaults to ``True``. - """ - def __init__(self, name, callback, **kwargs): - self.name = name - if not isinstance(name, str): - raise TypeError('Name of a command must be a string.') - - self.callback = callback - self.enabled = kwargs.get('enabled', True) - self.help = kwargs.get('help') - self.brief = kwargs.get('brief') - self.usage = kwargs.get('usage') - self.rest_is_raw = kwargs.get('rest_is_raw', False) - self.aliases = kwargs.get('aliases', []) - self.description = inspect.cleandoc(kwargs.get('description', '')) - self.hidden = kwargs.get('hidden', False) - signature = inspect.signature(callback) - self.params = signature.parameters.copy() - self.checks = kwargs.get('checks', []) - self.module = callback.__module__ - self.ignore_extra = kwargs.get('ignore_extra', True) - self.instance = None - self.parent = None - self._buckets = CooldownMapping(kwargs.get('cooldown')) - self._before_invoke = None - self._after_invoke = None - - @asyncio.coroutine - def dispatch_error(self, ctx, error): - ctx.command_failed = True - cog = self.instance - try: - coro = self.on_error - except AttributeError: - pass - else: - injected = wrap_callback(coro) - if cog is not None: - yield from injected(cog, ctx, error) - else: - yield from injected(ctx, error) - - try: - local = getattr(cog, '_{0.__class__.__name__}__error'.format(cog)) - except AttributeError: - pass - else: - wrapped = wrap_callback(local) - yield from wrapped(ctx, error) - finally: - ctx.bot.dispatch('command_error', ctx, error) - - def __get__(self, instance, owner): - if instance is not None: - self.instance = instance - return self - - @asyncio.coroutine - def do_conversion(self, ctx, converter, argument): - if converter is bool: - return _convert_to_bool(argument) - - if converter.__module__.startswith('discord.') and not converter.__module__.endswith('converter'): - converter = getattr(converters, converter.__name__ + 'Converter') - - if inspect.isclass(converter): - if issubclass(converter, converters.Converter): - instance = converter() - ret = yield from instance.convert(ctx, argument) - return ret - else: - method = getattr(converter, 'convert', None) - if method is not None and inspect.ismethod(method): - ret = yield from method(ctx, argument) - return ret - elif isinstance(converter, converters.Converter): - ret = yield from converter.convert(ctx, argument) - return ret - - return converter(argument) - - def _get_converter(self, param): - converter = param.annotation - if converter is param.empty: - if param.default is not param.empty: - converter = str if param.default is None else type(param.default) - else: - converter = str - return converter - - @asyncio.coroutine - def transform(self, ctx, param): - required = param.default is param.empty - converter = self._get_converter(param) - consume_rest_is_special = param.kind == param.KEYWORD_ONLY and not self.rest_is_raw - view = ctx.view - view.skip_ws() - - if view.eof: - if param.kind == param.VAR_POSITIONAL: - raise RuntimeError() # break the loop - if required: - raise MissingRequiredArgument(param) - return param.default - - if consume_rest_is_special: - argument = view.read_rest().strip() - else: - argument = quoted_word(view) - - try: - return (yield from self.do_conversion(ctx, converter, argument)) - except CommandError as e: - raise e - except Exception as e: - try: - name = converter.__name__ - except AttributeError: - name = converter.__class__.__name__ - - raise BadArgument('Converting to "{}" failed for parameter "{}".'.format(name, param.name)) from e - - @property - def clean_params(self): - """Retrieves the parameter OrderedDict without the context or self parameters. - - Useful for inspecting signature. - """ - result = self.params.copy() - if self.instance is not None: - # first parameter is self - result.popitem(last=False) - - try: - # first/second parameter is context - result.popitem(last=False) - except Exception as e: - raise ValueError('Missing context parameter') from None - - return result - - @property - def full_parent_name(self): - """Retrieves the fully qualified parent command name. - - This the base command name required to execute it. For example, - in ``?one two three`` the parent name would be ``one two``. - """ - entries = [] - command = self - while command.parent is not None: - command = command.parent - entries.append(command.name) - - return ' '.join(reversed(entries)) - - @property - def root_parent(self): - """Retrieves the root parent of this command. - - If the command has no parents then it returns ``None``. - - For example in commands ``?a b c test``, the root parent is - ``a``. - """ - entries = [] - command = self - while command.parent is not None: - command = command.parent - entries.append(command) - - if len(entries) == 0: - return None - - return entries[-1] - - @property - def qualified_name(self): - """Retrieves the fully qualified command name. - - This is the full parent name with the command name as well. - For example, in ``?one two three`` the qualified name would be - ``one two three``. - """ - - parent = self.full_parent_name - if parent: - return parent + ' ' + self.name - else: - return self.name - - def __str__(self): - return self.qualified_name - - @asyncio.coroutine - def _parse_arguments(self, ctx): - ctx.args = [ctx] if self.instance is None else [self.instance, ctx] - ctx.kwargs = {} - args = ctx.args - kwargs = ctx.kwargs - - view = ctx.view - iterator = iter(self.params.items()) - - if self.instance is not None: - # we have 'self' as the first parameter so just advance - # the iterator and resume parsing - try: - next(iterator) - except StopIteration: - fmt = 'Callback for {0.name} command is missing "self" parameter.' - raise discord.ClientException(fmt.format(self)) - - # next we have the 'ctx' as the next parameter - try: - next(iterator) - except StopIteration: - fmt = 'Callback for {0.name} command is missing "ctx" parameter.' - raise discord.ClientException(fmt.format(self)) - - for name, param in iterator: - if param.kind == param.POSITIONAL_OR_KEYWORD: - transformed = yield from self.transform(ctx, param) - args.append(transformed) - elif param.kind == param.KEYWORD_ONLY: - # kwarg only param denotes "consume rest" semantics - if self.rest_is_raw: - converter = self._get_converter(param) - argument = view.read_rest() - kwargs[name] = yield from self.do_conversion(ctx, converter, argument) - else: - kwargs[name] = yield from self.transform(ctx, param) - break - elif param.kind == param.VAR_POSITIONAL: - while not view.eof: - try: - transformed = yield from self.transform(ctx, param) - args.append(transformed) - except RuntimeError: - break - - if not self.ignore_extra: - if not view.eof: - raise TooManyArguments('Too many arguments passed to ' + self.qualified_name) - - @asyncio.coroutine - def _verify_checks(self, ctx): - if not self.enabled: - raise DisabledCommand('{0.name} command is disabled'.format(self)) - - if not (yield from self.can_run(ctx)): - raise CheckFailure('The check functions for command {0.qualified_name} failed.'.format(self)) - - @asyncio.coroutine - def call_before_hooks(self, ctx): - # now that we're done preparing we can call the pre-command hooks - # first, call the command local hook: - cog = self.instance - if self._before_invoke is not None: - if cog is None: - yield from self._before_invoke(ctx) - else: - yield from self._before_invoke(cog, ctx) - - # call the cog local hook if applicable: - try: - hook = getattr(cog, '_{0.__class__.__name__}__before_invoke'.format(cog)) - except AttributeError: - pass - else: - yield from hook(ctx) - - # call the bot global hook if necessary - hook = ctx.bot._before_invoke - if hook is not None: - yield from hook(ctx) - - @asyncio.coroutine - def call_after_hooks(self, ctx): - cog = self.instance - if self._after_invoke is not None: - if cog is None: - yield from self._after_invoke(ctx) - else: - yield from self._after_invoke(cog, ctx) - - try: - hook = getattr(cog, '_{0.__class__.__name__}__after_invoke'.format(cog)) - except AttributeError: - pass - else: - yield from hook(ctx) - - hook = ctx.bot._after_invoke - if hook is not None: - yield from hook(ctx) - - @asyncio.coroutine - def prepare(self, ctx): - ctx.command = self - yield from self._verify_checks(ctx) - - if self._buckets.valid: - bucket = self._buckets.get_bucket(ctx) - retry_after = bucket.is_rate_limited() - if retry_after: - raise CommandOnCooldown(bucket, retry_after) - - yield from self._parse_arguments(ctx) - yield from self.call_before_hooks(ctx) - - def reset_cooldown(self, ctx): - """Resets the cooldown on this command. - - Parameters - ----------- - ctx: :class:`.Context` - The invocation context to reset the cooldown under. - """ - if self._buckets.valid: - bucket = self._buckets.get_bucket(ctx) - bucket.reset() - - @asyncio.coroutine - def invoke(self, ctx): - yield from self.prepare(ctx) - - # terminate the invoked_subcommand chain. - # since we're in a regular command (and not a group) then - # the invoked subcommand is None. - ctx.invoked_subcommand = None - injected = hooked_wrapped_callback(self, ctx, self.callback) - yield from injected(*ctx.args, **ctx.kwargs) - - @asyncio.coroutine - def reinvoke(self, ctx, *, call_hooks=False): - ctx.command = self - yield from self._parse_arguments(ctx) - - if call_hooks: - yield from self.call_before_hooks(ctx) - - ctx.invoked_subcommand = None - try: - yield from self.callback(*ctx.args, **ctx.kwargs) - except: - ctx.command_failed = True - raise - finally: - if call_hooks: - yield from self.call_after_hooks(ctx) - - def error(self, coro): - """A decorator that registers a coroutine as a local error handler. - - A local error handler is an :func:`.on_command_error` event limited to - a single command. However, the :func:`.on_command_error` is still - invoked afterwards as the catch-all. - - Parameters - ----------- - coro - The coroutine to register as the local error handler. - - Raises - ------- - discord.ClientException - The coroutine is not actually a coroutine. - """ - - if not asyncio.iscoroutinefunction(coro): - raise discord.ClientException('The error handler must be a coroutine.') - - self.on_error = coro - return coro - - def before_invoke(self, coro): - """A decorator that registers a coroutine as a pre-invoke hook. - - A pre-invoke hook is called directly before the command is - called. This makes it a useful function to set up database - connections or any type of set up required. - - This pre-invoke hook takes a sole parameter, a :class:`.Context`. - - See :meth:`.Bot.before_invoke` for more info. - - Parameters - ----------- - coro - The coroutine to register as the pre-invoke hook. - - Raises - ------- - :exc:`.ClientException` - The coroutine is not actually a coroutine. - """ - if not asyncio.iscoroutinefunction(coro): - raise discord.ClientException('The error handler must be a coroutine.') - - self._before_invoke = coro - return coro - - def after_invoke(self, coro): - """A decorator that registers a coroutine as a post-invoke hook. - - A post-invoke hook is called directly after the command is - called. This makes it a useful function to clean-up database - connections or any type of clean up required. - - This post-invoke hook takes a sole parameter, a :class:`.Context`. - - See :meth:`.Bot.after_invoke` for more info. - - Parameters - ----------- - coro - The coroutine to register as the post-invoke hook. - - Raises - ------- - :exc:`.ClientException` - The coroutine is not actually a coroutine. - """ - if not asyncio.iscoroutinefunction(coro): - raise discord.ClientException('The error handler must be a coroutine.') - - self._after_invoke = coro - return coro - - @property - def cog_name(self): - """The name of the cog this command belongs to. None otherwise.""" - return type(self.instance).__name__ if self.instance is not None else None - - @property - def short_doc(self): - """Gets the "short" documentation of a command. - - By default, this is the :attr:`brief` attribute. - If that lookup leads to an empty string then the first line of the - :attr:`help` attribute is used instead. - """ - if self.brief: - return self.brief - if self.help: - return self.help.split('\n', 1)[0] - return '' - - @property - def signature(self): - """Returns a POSIX-like signature useful for help command output.""" - result = [] - parent = self.full_parent_name - if len(self.aliases) > 0: - aliases = '|'.join(self.aliases) - fmt = '[%s|%s]' % (self.name, aliases) - if parent: - fmt = parent + ' ' + fmt - result.append(fmt) - else: - name = self.name if not parent else parent + ' ' + self.name - result.append(name) - - if self.usage: - result.append(self.usage) - return ' '.join(result) - - params = self.clean_params - if not params: - return ' '.join(result) - - for name, param in params.items(): - if param.default is not param.empty: - # We don't want None or '' to trigger the [name=value] case and instead it should - # do [name] since [name=None] or [name=] are not exactly useful for the user. - should_print = param.default if isinstance(param.default, str) else param.default is not None - if should_print: - result.append('[%s=%s]' % (name, param.default)) - else: - result.append('[%s]' % name) - elif param.kind == param.VAR_POSITIONAL: - result.append('[%s...]' % name) - else: - result.append('<%s>' % name) - - return ' '.join(result) - - @asyncio.coroutine - def can_run(self, ctx): - """|coro| - - Checks if the command can be executed by checking all the predicates - inside the :attr:`.checks` attribute. - - Parameters - ----------- - ctx: :class:`.Context` - The ctx of the command currently being invoked. - - Returns - -------- - bool - A boolean indicating if the command can be invoked. - """ - - original = ctx.command - ctx.command = self - - try: - if not (yield from ctx.bot.can_run(ctx)): - raise CheckFailure('The global check functions for command {0.qualified_name} failed.'.format(self)) - - cog = self.instance - if cog is not None: - try: - local_check = getattr(cog, '_{0.__class__.__name__}__local_check'.format(cog)) - except AttributeError: - pass - else: - ret = yield from discord.utils.maybe_coroutine(local_check, ctx) - if not ret: - return False - - predicates = self.checks - if not predicates: - # since we have no checks, then we just return True. - return True - - return (yield from discord.utils.async_all(predicate(ctx) for predicate in predicates)) - finally: - ctx.command = original - -class GroupMixin: - """A mixin that implements common functionality for classes that behave - similar to :class:`.Group` and are allowed to register commands. - - Attributes - ----------- - all_commands: dict - A mapping of command name to :class:`.Command` or superclass - objects. - """ - def __init__(self, **kwargs): - self.all_commands = {} - super().__init__(**kwargs) - - @property - def commands(self): - """Set[:class:`.Command`]: A unique set of commands without aliases that are registered.""" - return set(self.all_commands.values()) - - def recursively_remove_all_commands(self): - for command in self.all_commands.copy().values(): - if isinstance(command, GroupMixin): - command.recursively_remove_all_commands() - self.remove_command(command.name) - - def add_command(self, command): - """Adds a :class:`.Command` or its superclasses into the internal list - of commands. - - This is usually not called, instead the :meth:`~.GroupMixin.command` or - :meth:`~.GroupMixin.group` shortcut decorators are used instead. - - Parameters - ----------- - command - The command to add. - - Raises - ------- - :exc:`.ClientException` - If the command is already registered. - TypeError - If the command passed is not a subclass of :class:`.Command`. - """ - - if not isinstance(command, Command): - raise TypeError('The command passed must be a subclass of Command') - - if isinstance(self, Command): - command.parent = self - - if command.name in self.all_commands: - raise discord.ClientException('Command {0.name} is already registered.'.format(command)) - - self.all_commands[command.name] = command - for alias in command.aliases: - if alias in self.all_commands: - raise discord.ClientException('The alias {} is already an existing command or alias.'.format(alias)) - self.all_commands[alias] = command - - def remove_command(self, name): - """Remove a :class:`.Command` or subclasses from the internal list - of commands. - - This could also be used as a way to remove aliases. - - Parameters - ----------- - name: str - The name of the command to remove. - - Returns - -------- - :class:`.Command` or subclass - The command that was removed. If the name is not valid then - `None` is returned instead. - """ - command = self.all_commands.pop(name, None) - - # does not exist - if command is None: - return None - - if name in command.aliases: - # we're removing an alias so we don't want to remove the rest - return command - - # we're not removing the alias so let's delete the rest of them. - for alias in command.aliases: - self.all_commands.pop(alias, None) - return command - - def walk_commands(self): - """An iterator that recursively walks through all commands and subcommands.""" - for command in tuple(self.all_commands.values()): - yield command - if isinstance(command, GroupMixin): - yield from command.walk_commands() - - def get_command(self, name): - """Get a :class:`.Command` or subclasses from the internal list - of commands. - - This could also be used as a way to get aliases. - - The name could be fully qualified (e.g. ``'foo bar'``) will get - the subcommand ``bar`` of the group command ``foo``. If a - subcommand is not found then ``None`` is returned just as usual. - - Parameters - ----------- - name: str - The name of the command to get. - - Returns - -------- - Command or subclass - The command that was requested. If not found, returns ``None``. - """ - - names = name.split() - obj = self.all_commands.get(names[0]) - if not isinstance(obj, GroupMixin): - return obj - - for name in names[1:]: - try: - obj = obj.all_commands[name] - except (AttributeError, KeyError): - return None - - return obj - - def command(self, *args, **kwargs): - """A shortcut decorator that invokes :func:`.command` and adds it to - the internal command list via :meth:`~.GroupMixin.add_command`. - """ - def decorator(func): - result = command(*args, **kwargs)(func) - self.add_command(result) - return result - - return decorator - - def group(self, *args, **kwargs): - """A shortcut decorator that invokes :func:`.group` and adds it to - the internal command list via :meth:`~.GroupMixin.add_command`. - """ - def decorator(func): - result = group(*args, **kwargs)(func) - self.add_command(result) - return result - - return decorator - -class Group(GroupMixin, Command): - """A class that implements a grouping protocol for commands to be - executed as subcommands. - - This class is a subclass of :class:`.Command` and thus all options - valid in :class:`.Command` are valid in here as well. - - Attributes - ----------- - invoke_without_command: bool - Indicates if the group callback should begin parsing and - invocation only if no subcommand was found. Useful for - making it an error handling function to tell the user that - no subcommand was found or to have different functionality - in case no subcommand was found. If this is ``False``, then - the group callback will always be invoked first. This means - that the checks and the parsing dictated by its parameters - will be executed. Defaults to ``False``. - """ - def __init__(self, **attrs): - self.invoke_without_command = attrs.pop('invoke_without_command', False) - super().__init__(**attrs) - - @asyncio.coroutine - def invoke(self, ctx): - early_invoke = not self.invoke_without_command - if early_invoke: - yield from self.prepare(ctx) - - view = ctx.view - previous = view.index - view.skip_ws() - trigger = view.get_word() - - if trigger: - ctx.subcommand_passed = trigger - ctx.invoked_subcommand = self.all_commands.get(trigger, None) - - if early_invoke: - injected = hooked_wrapped_callback(self, ctx, self.callback) - yield from injected(*ctx.args, **ctx.kwargs) - - if trigger and ctx.invoked_subcommand: - ctx.invoked_with = trigger - yield from ctx.invoked_subcommand.invoke(ctx) - elif not early_invoke: - # undo the trigger parsing - view.index = previous - view.previous = previous - yield from super().invoke(ctx) - - @asyncio.coroutine - def reinvoke(self, ctx, *, call_hooks=False): - early_invoke = not self.invoke_without_command - if early_invoke: - ctx.command = self - yield from self._parse_arguments(ctx) - - if call_hooks: - yield from self.call_before_hooks(ctx) - - view = ctx.view - previous = view.index - view.skip_ws() - trigger = view.get_word() - - if trigger: - ctx.subcommand_passed = trigger - ctx.invoked_subcommand = self.all_commands.get(trigger, None) - - if early_invoke: - try: - yield from self.callback(*ctx.args, **ctx.kwargs) - except: - ctx.command_failed = True - raise - finally: - if call_hooks: - yield from self.call_after_hooks(ctx) - - if trigger and ctx.invoked_subcommand: - ctx.invoked_with = trigger - yield from ctx.invoked_subcommand.reinvoke(ctx, call_hooks=call_hooks) - elif not early_invoke: - # undo the trigger parsing - view.index = previous - view.previous = previous - yield from super().reinvoke(ctx, call_hooks=call_hooks) - -# Decorators - -def command(name=None, cls=None, **attrs): - """A decorator that transforms a function into a :class:`.Command` - or if called with :func:`.group`, :class:`.Group`. - - By default the ``help`` attribute is received automatically from the - docstring of the function and is cleaned up with the use of - ``inspect.cleandoc``. If the docstring is ``bytes``, then it is decoded - into ``str`` using utf-8 encoding. - - All checks added using the :func:`.check` & co. decorators are added into - the function. There is no way to supply your own checks through this - decorator. - - Parameters - ----------- - name: str - The name to create the command with. By default this uses the - function name unchanged. - cls - The class to construct with. By default this is :class:`.Command`. - You usually do not change this. - attrs - Keyword arguments to pass into the construction of the class denoted - by ``cls``. - - Raises - ------- - TypeError - If the function is not a coroutine or is already a command. - """ - if cls is None: - cls = Command - - def decorator(func): - if isinstance(func, Command): - raise TypeError('Callback is already a command.') - if not asyncio.iscoroutinefunction(func): - raise TypeError('Callback must be a coroutine.') - - try: - checks = func.__commands_checks__ - checks.reverse() - del func.__commands_checks__ - except AttributeError: - checks = [] - - try: - cooldown = func.__commands_cooldown__ - del func.__commands_cooldown__ - except AttributeError: - cooldown = None - - help_doc = attrs.get('help') - if help_doc is not None: - help_doc = inspect.cleandoc(help_doc) - else: - help_doc = inspect.getdoc(func) - if isinstance(help_doc, bytes): - help_doc = help_doc.decode('utf-8') - - attrs['help'] = help_doc - fname = name or func.__name__ - return cls(name=fname, callback=func, checks=checks, cooldown=cooldown, **attrs) - - return decorator - -def group(name=None, **attrs): - """A decorator that transforms a function into a :class:`.Group`. - - This is similar to the :func:`.command` decorator but creates a - :class:`.Group` instead of a :class:`.Command`. - """ - return command(name=name, cls=Group, **attrs) - -def check(predicate): - """A decorator that adds a check to the :class:`.Command` or its - subclasses. These checks could be accessed via :attr:`.Command.checks`. - - These checks should be predicates that take in a single parameter taking - a :class:`.Context`. If the check returns a ``False``\-like value then - during invocation a :exc:`.CheckFailure` exception is raised and sent to - the :func:`.on_command_error` event. - - If an exception should be thrown in the predicate then it should be a - subclass of :exc:`.CommandError`. Any exception not subclassed from it - will be propagated while those subclassed will be sent to - :func:`.on_command_error`. - - .. note:: - - These functions can either be regular functions or coroutines. - - Parameters - ----------- - predicate - The predicate to check if the command should be invoked. - - Examples - --------- - - Creating a basic check to see if the command invoker is you. - - .. code-block:: python3 - - def check_if_it_is_me(ctx): - return ctx.message.author.id == 85309593344815104 - - @bot.command() - @commands.check(check_if_it_is_me) - async def only_for_me(ctx): - await ctx.send('I know you!') - - Transforming common checks into its own decorator: - - .. code-block:: python3 - - def is_me(): - def predicate(ctx): - return ctx.message.author.id == 85309593344815104 - return commands.check(predicate) - - @bot.command() - @is_me() - async def only_me(ctx): - await ctx.send('Only you!') - - """ - - def decorator(func): - if isinstance(func, Command): - func.checks.append(predicate) - else: - if not hasattr(func, '__commands_checks__'): - func.__commands_checks__ = [] - - func.__commands_checks__.append(predicate) - - return func - return decorator - -def has_role(name): - """A :func:`.check` that is added that checks if the member invoking the - command has the role specified via the name specified. - - The name is case sensitive and must be exact. No normalisation is done in - the input. - - If the message is invoked in a private message context then the check will - return ``False``. - - Parameters - ----------- - name: str - The name of the role to check. - """ - - def predicate(ctx): - if not isinstance(ctx.channel, discord.abc.GuildChannel): - return False - - role = discord.utils.get(ctx.author.roles, name=name) - return role is not None - - return check(predicate) - -def has_any_role(*names): - """A :func:`.check` that is added that checks if the member invoking the - command has **any** of the roles specified. This means that if they have - one out of the three roles specified, then this check will return `True`. - - Similar to :func:`.has_role`\, the names passed in must be exact. - - Parameters - ----------- - names - An argument list of names to check that the member has roles wise. - - Example - -------- - - .. code-block:: python3 - - @bot.command() - @commands.has_any_role('Library Devs', 'Moderators') - async def cool(ctx): - await ctx.send('You are cool indeed') - """ - def predicate(ctx): - if not isinstance(ctx.channel, discord.abc.GuildChannel): - return False - - getter = functools.partial(discord.utils.get, ctx.author.roles) - return any(getter(name=name) is not None for name in names) - return check(predicate) - -def has_permissions(**perms): - """A :func:`.check` that is added that checks if the member has any of - the permissions necessary. - - The permissions passed in must be exactly like the properties shown under - :class:`.discord.Permissions`. - - This check raises a special exception, :exc:`.MissingPermissions` - that is derived from :exc:`.CheckFailure`. - - Parameters - ------------ - perms - An argument list of permissions to check for. - - Example - --------- - - .. code-block:: python3 - - @bot.command() - @commands.has_permissions(manage_messages=True) - async def test(ctx): - await ctx.send('You can manage messages.') - - """ - def predicate(ctx): - ch = ctx.channel - permissions = ch.permissions_for(ctx.author) - - missing = [perm for perm, value in perms.items() if getattr(permissions, perm, None) != value] - - if not missing: - return True - - raise MissingPermissions(missing) - - return check(predicate) - -def bot_has_role(name): - """Similar to :func:`.has_role` except checks if the bot itself has the - role. - """ - - def predicate(ctx): - ch = ctx.channel - if not isinstance(ch, discord.abc.GuildChannel): - return False - me = ch.guild.me - role = discord.utils.get(me.roles, name=name) - return role is not None - return check(predicate) - -def bot_has_any_role(*names): - """Similar to :func:`.has_any_role` except checks if the bot itself has - any of the roles listed. - """ - def predicate(ctx): - ch = ctx.channel - if not isinstance(ch, discord.abc.GuildChannel): - return False - me = ch.guild.me - getter = functools.partial(discord.utils.get, me.roles) - return any(getter(name=name) is not None for name in names) - return check(predicate) - -def bot_has_permissions(**perms): - """Similar to :func:`.has_permissions` except checks if the bot itself has - the permissions listed. - - This check raises a special exception, :exc:`.BotMissingPermissions` - that is derived from :exc:`.CheckFailure`. - """ - def predicate(ctx): - guild = ctx.guild - me = guild.me if guild is not None else ctx.bot.user - permissions = ctx.channel.permissions_for(me) - - missing = [perm for perm, value in perms.items() if getattr(permissions, perm, None) != value] - - if not missing: - return True - - raise BotMissingPermissions(missing) - - return check(predicate) - -def guild_only(): - """A :func:`.check` that indicates this command must only be used in a - guild context only. Basically, no private messages are allowed when - using the command. - - This check raises a special exception, :exc:`.NoPrivateMessage` - that is derived from :exc:`.CheckFailure`. - """ - - def predicate(ctx): - if ctx.guild is None: - raise NoPrivateMessage('This command cannot be used in private messages.') - return True - - return check(predicate) - -def is_owner(): - """A :func:`.check` that checks if the person invoking this command is the - owner of the bot. - - This is powered by :meth:`.Bot.is_owner`. - - This check raises a special exception, :exc:`.NotOwner` that is derived - from :exc:`.CheckFailure`. - """ - - @asyncio.coroutine - def predicate(ctx): - if not (yield from ctx.bot.is_owner(ctx.author)): - raise NotOwner('You do not own this bot.') - return True - - return check(predicate) - -def is_nsfw(): - """A :func:`.check` that checks if the channel is a NSFW channel.""" - def pred(ctx): - return isinstance(ctx.channel, discord.TextChannel) and ctx.channel.is_nsfw() - return check(pred) - -def cooldown(rate, per, type=BucketType.default): - """A decorator that adds a cooldown to a :class:`.Command` - or its subclasses. - - A cooldown allows a command to only be used a specific amount - of times in a specific time frame. These cooldowns can be based - either on a per-guild, per-channel, per-user, or global basis. - Denoted by the third argument of ``type`` which must be of enum - type ``BucketType`` which could be either: - - - ``BucketType.default`` for a global basis. - - ``BucketType.user`` for a per-user basis. - - ``BucketType.guild`` for a per-guild basis. - - ``BucketType.channel`` for a per-channel basis. - - If a cooldown is triggered, then :exc:`.CommandOnCooldown` is triggered in - :func:`.on_command_error` and the local error handler. - - A command can only have a single cooldown. - - Parameters - ------------ - rate: int - The number of times a command can be used before triggering a cooldown. - per: float - The amount of seconds to wait for a cooldown when it's been triggered. - type: ``BucketType`` - The type of cooldown to have. - """ - - def decorator(func): - if isinstance(func, Command): - func._buckets = CooldownMapping(Cooldown(rate, per, type)) - else: - func.__commands_cooldown__ = Cooldown(rate, per, type) - return func - return decorator diff --git a/discord.py-rewrite/discord/ext/commands/errors.py b/discord.py-rewrite/discord/ext/commands/errors.py deleted file mode 100644 index c7d8774..0000000 --- a/discord.py-rewrite/discord/ext/commands/errors.py +++ /dev/null @@ -1,180 +0,0 @@ -# -*- coding: utf-8 -*- -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from discord.errors import DiscordException - - -__all__ = [ 'CommandError', 'MissingRequiredArgument', 'BadArgument', - 'NoPrivateMessage', 'CheckFailure', 'CommandNotFound', - 'DisabledCommand', 'CommandInvokeError', 'TooManyArguments', - 'UserInputError', 'CommandOnCooldown', 'NotOwner', - 'MissingPermissions', 'BotMissingPermissions'] - -class CommandError(DiscordException): - """The base exception type for all command related errors. - - This inherits from :exc:`discord.DiscordException`. - - This exception and exceptions derived from it are handled - in a special way as they are caught and passed into a special event - from :class:`.Bot`\, :func:`on_command_error`. - """ - def __init__(self, message=None, *args): - if message is not None: - # clean-up @everyone and @here mentions - m = message.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere') - super().__init__(m, *args) - else: - super().__init__(*args) - -class UserInputError(CommandError): - """The base exception type for errors that involve errors - regarding user input. - - This inherits from :exc:`.CommandError`. - """ - pass - -class CommandNotFound(CommandError): - """Exception raised when a command is attempted to be invoked - but no command under that name is found. - - This is not raised for invalid subcommands, rather just the - initial main command that is attempted to be invoked. - """ - pass - -class MissingRequiredArgument(UserInputError): - """Exception raised when parsing a command and a parameter - that is required is not encountered. - - Attributes - ----------- - param: str - The argument that is missing. - """ - def __init__(self, param): - self.param = param.name - super().__init__('{0.name} is a required argument that is missing.'.format(param)) - -class TooManyArguments(UserInputError): - """Exception raised when the command was passed too many arguments and its - :attr:`.Command.ignore_extra` attribute was not set to ``True``. - """ - pass - -class BadArgument(UserInputError): - """Exception raised when a parsing or conversion failure is encountered - on an argument to pass into a command. - """ - pass - -class CheckFailure(CommandError): - """Exception raised when the predicates in :attr:`.Command.checks` have failed.""" - pass - -class NoPrivateMessage(CheckFailure): - """Exception raised when an operation does not work in private message - contexts. - """ - pass - -class NotOwner(CheckFailure): - """Exception raised when the message author is not the owner of the bot.""" - pass - -class DisabledCommand(CommandError): - """Exception raised when the command being invoked is disabled.""" - pass - -class CommandInvokeError(CommandError): - """Exception raised when the command being invoked raised an exception. - - Attributes - ----------- - original - The original exception that was raised. You can also get this via - the ``__cause__`` attribute. - """ - def __init__(self, e): - self.original = e - super().__init__('Command raised an exception: {0.__class__.__name__}: {0}'.format(e)) - -class CommandOnCooldown(CommandError): - """Exception raised when the command being invoked is on cooldown. - - Attributes - ----------- - cooldown: Cooldown - A class with attributes ``rate``, ``per``, and ``type`` similar to - the :func:`.cooldown` decorator. - retry_after: float - The amount of seconds to wait before you can retry again. - """ - def __init__(self, cooldown, retry_after): - self.cooldown = cooldown - self.retry_after = retry_after - super().__init__('You are on cooldown. Try again in {:.2f}s'.format(retry_after)) - -class MissingPermissions(CheckFailure): - """Exception raised when the command invoker lacks permissions to run - command. - - Attributes - ----------- - missing_perms: list - The required permissions that are missing. - """ - def __init__(self, missing_perms, *args): - self.missing_perms = missing_perms - - missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_perms] - - if len(missing) > 2: - fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1]) - else: - fmt = ' and '.join(missing) - message = 'You are missing {} permission(s) to run command.'.format(fmt) - super().__init__(message, *args) - -class BotMissingPermissions(CheckFailure): - """Exception raised when the bot lacks permissions to run command. - - Attributes - ----------- - missing_perms: list - The required permissions that are missing. - """ - def __init__(self, missing_perms, *args): - self.missing_perms = missing_perms - - missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_perms] - - if len(missing) > 2: - fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1]) - else: - fmt = ' and '.join(missing) - message = 'Bot requires {} permission(s) to run command.'.format(fmt) - super().__init__(message, *args) diff --git a/discord.py-rewrite/discord/ext/commands/formatter.py b/discord.py-rewrite/discord/ext/commands/formatter.py deleted file mode 100644 index 8e30f58..0000000 --- a/discord.py-rewrite/discord/ext/commands/formatter.py +++ /dev/null @@ -1,346 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import itertools -import inspect -import asyncio - -from .core import GroupMixin, Command -from .errors import CommandError -# from discord.iterators import _FilteredAsyncIterator - -# help -> shows info of bot on top/bottom and lists subcommands -# help command -> shows detailed info of command -# help command -> same as above - -# - -# - -# - -# Cog: -# -# -# Other Cog: -# -# No Category: -# - -# Type help command for more info on a command. -# You can also type help category for more info on a category. - -class Paginator: - """A class that aids in paginating code blocks for Discord messages. - - Attributes - ----------- - prefix: str - The prefix inserted to every page. e.g. three backticks. - suffix: str - The suffix appended at the end of every page. e.g. three backticks. - max_size: int - The maximum amount of codepoints allowed in a page. - """ - def __init__(self, prefix='```', suffix='```', max_size=2000): - self.prefix = prefix - self.suffix = suffix - self.max_size = max_size - len(suffix) - self._current_page = [prefix] - self._count = len(prefix) + 1 # prefix + newline - self._pages = [] - - def add_line(self, line='', *, empty=False): - """Adds a line to the current page. - - If the line exceeds the :attr:`max_size` then an exception - is raised. - - Parameters - ----------- - line: str - The line to add. - empty: bool - Indicates if another empty line should be added. - - Raises - ------ - RuntimeError - The line was too big for the current :attr:`max_size`. - """ - if len(line) > self.max_size - len(self.prefix) - 2: - raise RuntimeError('Line exceeds maximum page size %s' % (self.max_size - len(self.prefix) - 2)) - - if self._count + len(line) + 1 > self.max_size: - self.close_page() - - self._count += len(line) + 1 - self._current_page.append(line) - - if empty: - self._current_page.append('') - self._count += 1 - - def close_page(self): - """Prematurely terminate a page.""" - self._current_page.append(self.suffix) - self._pages.append('\n'.join(self._current_page)) - self._current_page = [self.prefix] - self._count = len(self.prefix) + 1 # prefix + newline - - @property - def pages(self): - """Returns the rendered list of pages.""" - # we have more than just the prefix in our current page - if len(self._current_page) > 1: - self.close_page() - return self._pages - - def __repr__(self): - fmt = '' - return fmt.format(self) - -class HelpFormatter: - """The default base implementation that handles formatting of the help - command. - - To override the behaviour of the formatter, :meth:`~.HelpFormatter.format` - should be overridden. A number of utility functions are provided for use - inside that method. - - Attributes - ----------- - show_hidden: bool - Dictates if hidden commands should be shown in the output. - Defaults to ``False``. - show_check_failure: bool - Dictates if commands that have their :attr:`.Command.checks` failed - shown. Defaults to ``False``. - width: int - The maximum number of characters that fit in a line. - Defaults to 80. - """ - def __init__(self, show_hidden=False, show_check_failure=False, width=80): - self.width = width - self.show_hidden = show_hidden - self.show_check_failure = show_check_failure - - def has_subcommands(self): - """bool: Specifies if the command has subcommands.""" - return isinstance(self.command, GroupMixin) - - def is_bot(self): - """bool: Specifies if the command being formatted is the bot itself.""" - return self.command is self.context.bot - - def is_cog(self): - """bool: Specifies if the command being formatted is actually a cog.""" - return not self.is_bot() and not isinstance(self.command, Command) - - def shorten(self, text): - """Shortens text to fit into the :attr:`width`.""" - if len(text) > self.width: - return text[:self.width - 3] + '...' - return text - - @property - def max_name_size(self): - """int: Returns the largest name length of a command or if it has subcommands - the largest subcommand name.""" - try: - commands = self.command.all_commands if not self.is_cog() else self.context.bot.all_commands - if commands: - return max(map(lambda c: len(c.name) if self.show_hidden or not c.hidden else 0, commands.values())) - return 0 - except AttributeError: - return len(self.command.name) - - @property - def clean_prefix(self): - """The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``.""" - user = self.context.bot.user - # this breaks if the prefix mention is not the bot itself but I - # consider this to be an *incredibly* strange use case. I'd rather go - # for this common use case rather than waste performance for the - # odd one. - return self.context.prefix.replace(user.mention, '@' + user.name) - - def get_command_signature(self): - """Retrieves the signature portion of the help page.""" - prefix = self.clean_prefix - cmd = self.command - return prefix + cmd.signature - - def get_ending_note(self): - command_name = self.context.invoked_with - return "Type {0}{1} command for more info on a command.\n" \ - "You can also type {0}{1} category for more info on a category.".format(self.clean_prefix, command_name) - - @asyncio.coroutine - def filter_command_list(self): - """Returns a filtered list of commands based on the two attributes - provided, :attr:`show_check_failure` and :attr:`show_hidden`. - Also filters based on if :meth:`~.HelpFormatter.is_cog` is valid. - - Returns - -------- - iterable - An iterable with the filter being applied. The resulting value is - a (key, value) tuple of the command name and the command itself. - """ - - def sane_no_suspension_point_predicate(tup): - cmd = tup[1] - if self.is_cog(): - # filter commands that don't exist to this cog. - if cmd.instance is not self.command: - return False - - if cmd.hidden and not self.show_hidden: - return False - - return True - - @asyncio.coroutine - def predicate(tup): - if sane_no_suspension_point_predicate(tup) is False: - return False - - cmd = tup[1] - try: - return (yield from cmd.can_run(self.context)) - except CommandError: - return False - - iterator = self.command.all_commands.items() if not self.is_cog() else self.context.bot.all_commands.items() - if self.show_check_failure: - return filter(sane_no_suspension_point_predicate, iterator) - - # Gotta run every check and verify it - ret = [] - for elem in iterator: - valid = yield from predicate(elem) - if valid: - ret.append(elem) - - return ret - - def _add_subcommands_to_page(self, max_width, commands): - for name, command in commands: - if name in command.aliases: - # skip aliases - continue - - entry = ' {0:<{width}} {1}'.format(name, command.short_doc, width=max_width) - shortened = self.shorten(entry) - self._paginator.add_line(shortened) - - @asyncio.coroutine - def format_help_for(self, context, command_or_bot): - """Formats the help page and handles the actual heavy lifting of how - the help command looks like. To change the behaviour, override the - :meth:`~.HelpFormatter.format` method. - - Parameters - ----------- - context: :class:`.Context` - The context of the invoked help command. - command_or_bot: :class:`.Command` or :class:`.Bot` - The bot or command that we are getting the help of. - - Returns - -------- - list - A paginated output of the help command. - """ - self.context = context - self.command = command_or_bot - return (yield from self.format()) - - @asyncio.coroutine - def format(self): - """Handles the actual behaviour involved with formatting. - - To change the behaviour, this method should be overridden. - - Returns - -------- - list - A paginated output of the help command. - """ - self._paginator = Paginator() - - # we need a padding of ~80 or so - - description = self.command.description if not self.is_cog() else inspect.getdoc(self.command) - - if description: - # portion - self._paginator.add_line(description, empty=True) - - if isinstance(self.command, Command): - # - signature = self.get_command_signature() - self._paginator.add_line(signature, empty=True) - - # section - if self.command.help: - self._paginator.add_line(self.command.help, empty=True) - - # end it here if it's just a regular command - if not self.has_subcommands(): - self._paginator.close_page() - return self._paginator.pages - - max_width = self.max_name_size - - def category(tup): - cog = tup[1].cog_name - # we insert the zero width space there to give it approximate - # last place sorting position. - return cog + ':' if cog is not None else '\u200bNo Category:' - - filtered = yield from self.filter_command_list() - if self.is_bot(): - data = sorted(filtered, key=category) - for category, commands in itertools.groupby(data, key=category): - # there simply is no prettier way of doing this. - commands = sorted(commands) - if len(commands) > 0: - self._paginator.add_line(category) - - self._add_subcommands_to_page(max_width, commands) - else: - filtered = sorted(filtered) - if filtered: - self._paginator.add_line('Commands:') - self._add_subcommands_to_page(max_width, filtered) - - # add the ending note - self._paginator.add_line() - ending_note = self.get_ending_note() - self._paginator.add_line(ending_note) - return self._paginator.pages diff --git a/discord.py-rewrite/discord/ext/commands/view.py b/discord.py-rewrite/discord/ext/commands/view.py deleted file mode 100644 index acde0b7..0000000 --- a/discord.py-rewrite/discord/ext/commands/view.py +++ /dev/null @@ -1,167 +0,0 @@ -# -*- coding: utf-8 -*- -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from .errors import BadArgument - -class StringView: - def __init__(self, buffer): - self.index = 0 - self.buffer = buffer - self.end = len(buffer) - self.previous = 0 - - @property - def current(self): - return None if self.eof else self.buffer[self.index] - - @property - def eof(self): - return self.index >= self.end - - def undo(self): - self.index = self.previous - - def skip_ws(self): - pos = 0 - while not self.eof: - try: - current = self.buffer[self.index + pos] - if not current.isspace(): - break - pos += 1 - except IndexError: - break - - self.previous = self.index - self.index += pos - return self.previous != self.index - - def skip_string(self, string): - strlen = len(string) - if self.buffer[self.index:self.index + strlen] == string: - self.previous = self.index - self.index += strlen - return True - return False - - def read_rest(self): - result = self.buffer[self.index:] - self.previous = self.index - self.index = self.end - return result - - def read(self, n): - result = self.buffer[self.index:self.index + n] - self.previous = self.index - self.index += n - return result - - def get(self): - try: - result = self.buffer[self.index + 1] - except IndexError: - result = None - - self.previous = self.index - self.index += 1 - return result - - def get_word(self): - pos = 0 - while not self.eof: - try: - current = self.buffer[self.index + pos] - if current.isspace(): - break - pos += 1 - except IndexError: - break - self.previous = self.index - result = self.buffer[self.index:self.index + pos] - self.index += pos - return result - - def __repr__(self): - return ''.format(self) - -# Parser - -def quoted_word(view): - current = view.current - - if current is None: - return None - - is_quoted = current == '"' - result = [] if is_quoted else [current] - - while not view.eof: - current = view.get() - if not current: - if is_quoted: - # unexpected EOF - raise BadArgument('Expected closing "') - return ''.join(result) - - # currently we accept strings in the format of "hello world" - # to embed a quote inside the string you must escape it: "a \"world\"" - if current == '\\': - next_char = view.get() - if not next_char: - # string ends with \ and no character after it - if is_quoted: - # if we're quoted then we're expecting a closing quote - raise BadArgument('Expected closing "') - # if we aren't then we just let it through - return ''.join(result) - - if next_char == '"': - # escaped quote - result.append('"') - else: - # different escape character, ignore it - view.undo() - result.append(current) - continue - - # closing quote - if current == '"': - next_char = view.get() - valid_eof = not next_char or next_char.isspace() - if is_quoted: - if not valid_eof: - raise BadArgument('Expected space after closing quotation') - - # we're quoted so it's okay - return ''.join(result) - else: - # we aren't quoted - raise BadArgument('Unexpected quote mark in non-quoted string') - - if current.isspace() and not is_quoted: - # end of word found - return ''.join(result) - - result.append(current) diff --git a/discord.py-rewrite/discord/file.py b/discord.py-rewrite/discord/file.py deleted file mode 100644 index c4733a3..0000000 --- a/discord.py-rewrite/discord/file.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import os.path - -class File: - """A parameter object used for :meth:`abc.Messageable.send` - for sending file objects. - - Attributes - ----------- - fp: Union[str, BinaryIO] - A file-like object opened in binary mode and read mode - or a filename representing a file in the hard drive to - open. - - .. note:: - - If the file-like object passed is opened via ``open`` then the - modes 'rb' should be used. - - To pass binary data, consider usage of ``io.BytesIO``. - - filename: Optional[str] - The filename to display when uploading to Discord. - If this is not given then it defaults to ``fp.name`` or if ``fp`` is - a string then the ``filename`` will default to the string given. - """ - - __slots__ = ('fp', 'filename', '_true_fp') - - def __init__(self, fp, filename=None): - self.fp = fp - self._true_fp = None - - if filename is None: - if isinstance(fp, str): - _, self.filename = os.path.split(fp) - else: - self.filename = getattr(fp, 'name', None) - else: - self.filename = filename - - def open_file(self): - fp = self.fp - if isinstance(fp, str): - self._true_fp = fp = open(fp, 'rb') - return fp - - def close(self): - if self._true_fp: - self._true_fp.close() diff --git a/discord.py-rewrite/discord/game.py b/discord.py-rewrite/discord/game.py deleted file mode 100644 index 238f22e..0000000 --- a/discord.py-rewrite/discord/game.py +++ /dev/null @@ -1,87 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -class Game: - """Represents a Discord game. - - .. container:: operations - - .. describe:: x == y - - Checks if two games are equal. - - .. describe:: x != y - - Checks if two games are not equal. - - .. describe:: hash(x) - - Returns the game's hash. - - .. describe:: str(x) - - Returns the game's name. - - Attributes - ----------- - name: str - The game's name. - url: str - The game's URL. Usually used for twitch streaming. - type: int - The type of game being played. 1 indicates "Streaming". - """ - - __slots__ = ('name', 'type', 'url') - - def __init__(self, **kwargs): - self.name = kwargs.get('name') - self.url = kwargs.get('url') - self.type = kwargs.get('type', 0) - - def __str__(self): - return str(self.name) - - def __repr__(self): - return ''.format(self) - - def _iterator(self): - for attr in self.__slots__: - value = getattr(self, attr, None) - if value is not None: - yield (attr, value) - - def __iter__(self): - return self._iterator() - - def __eq__(self, other): - return isinstance(other, Game) and other.name == self.name - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return hash(self.name) diff --git a/discord.py-rewrite/discord/gateway.py b/discord.py-rewrite/discord/gateway.py deleted file mode 100644 index 0ab0276..0000000 --- a/discord.py-rewrite/discord/gateway.py +++ /dev/null @@ -1,699 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import sys -import time -import websockets -import asyncio - -from . import utils, compat -from .game import Game -from .errors import ConnectionClosed, InvalidArgument -import logging -import zlib, json -from collections import namedtuple -import threading -import struct - -log = logging.getLogger(__name__) - -__all__ = [ 'DiscordWebSocket', 'KeepAliveHandler', 'VoiceKeepAliveHandler', - 'DiscordVoiceWebSocket', 'ResumeWebSocket' ] - -class ResumeWebSocket(Exception): - """Signals to initialise via RESUME opcode instead of IDENTIFY.""" - def __init__(self, shard_id): - self.shard_id = shard_id - -EventListener = namedtuple('EventListener', 'predicate event result future') - -class KeepAliveHandler(threading.Thread): - def __init__(self, *args, **kwargs): - ws = kwargs.pop('ws', None) - interval = kwargs.pop('interval', None) - shard_id = kwargs.pop('shard_id', None) - threading.Thread.__init__(self, *args, **kwargs) - self.ws = ws - self.interval = interval - self.daemon = True - self.shard_id = shard_id - self.msg = 'Keeping websocket alive with sequence %s.' - self._stop_ev = threading.Event() - self._last_ack = time.monotonic() - self._last_send = time.monotonic() - self.heartbeat_timeout = ws._max_heartbeat_timeout - - def run(self): - while not self._stop_ev.wait(self.interval): - if self._last_ack + self.heartbeat_timeout < time.monotonic(): - log.warn("Shard ID %s has stopped responding to the gateway. Closing and restarting." % self.shard_id) - coro = self.ws.close(1006) - f = compat.run_coroutine_threadsafe(coro, loop=self.ws.loop) - - try: - f.result() - except: - pass - finally: - self.stop() - return - - data = self.get_payload() - log.debug(self.msg, data['d']) - coro = self.ws.send_as_json(data) - f = compat.run_coroutine_threadsafe(coro, loop=self.ws.loop) - try: - # block until sending is complete - f.result() - except Exception: - self.stop() - else: - self._last_send = time.monotonic() - - def get_payload(self): - return { - 'op': self.ws.HEARTBEAT, - 'd': self.ws.sequence - } - - def stop(self): - self._stop_ev.set() - - def ack(self): - self._last_ack = time.monotonic() - -class VoiceKeepAliveHandler(KeepAliveHandler): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.msg = 'Keeping voice websocket alive with timestamp %s.' - - def get_payload(self): - return { - 'op': self.ws.HEARTBEAT, - 'd': int(time.time() * 1000) - } - -class DiscordWebSocket(websockets.client.WebSocketClientProtocol): - """Implements a WebSocket for Discord's gateway v6. - - This is created through :func:`create_main_websocket`. Library - users should never create this manually. - - Attributes - ----------- - DISPATCH - Receive only. Denotes an event to be sent to Discord, such as READY. - HEARTBEAT - When received tells Discord to keep the connection alive. - When sent asks if your connection is currently alive. - IDENTIFY - Send only. Starts a new session. - PRESENCE - Send only. Updates your presence. - VOICE_STATE - Send only. Starts a new connection to a voice guild. - VOICE_PING - Send only. Checks ping time to a voice guild, do not use. - RESUME - Send only. Resumes an existing connection. - RECONNECT - Receive only. Tells the client to reconnect to a new gateway. - REQUEST_MEMBERS - Send only. Asks for the full member list of a guild. - INVALIDATE_SESSION - Receive only. Tells the client to optionally invalidate the session - and IDENTIFY again. - HELLO - Receive only. Tells the client the heartbeat interval. - HEARTBEAT_ACK - Receive only. Confirms receiving of a heartbeat. Not having it implies - a connection issue. - GUILD_SYNC - Send only. Requests a guild sync. - gateway - The gateway we are currently connected to. - token - The authentication token for discord. - """ - - DISPATCH = 0 - HEARTBEAT = 1 - IDENTIFY = 2 - PRESENCE = 3 - VOICE_STATE = 4 - VOICE_PING = 5 - RESUME = 6 - RECONNECT = 7 - REQUEST_MEMBERS = 8 - INVALIDATE_SESSION = 9 - HELLO = 10 - HEARTBEAT_ACK = 11 - GUILD_SYNC = 12 - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.max_size = None - # an empty dispatcher to prevent crashes - self._dispatch = lambda *args: None - # generic event listeners - self._dispatch_listeners = [] - # the keep alive - self._keep_alive = None - - # ws related stuff - self.session_id = None - self.sequence = None - - @classmethod - @asyncio.coroutine - def from_client(cls, client, *, shard_id=None, session=None, sequence=None, resume=False): - """Creates a main websocket for Discord from a :class:`Client`. - - This is for internal use only. - """ - gateway = yield from client.http.get_gateway() - ws = yield from websockets.connect(gateway, loop=client.loop, klass=cls) - - # dynamically add attributes needed - ws.token = client.http.token - ws._connection = client._connection - ws._dispatch = client.dispatch - ws.gateway = gateway - ws.shard_id = shard_id - ws.shard_count = client._connection.shard_count - ws.session_id = session - ws.sequence = sequence - ws._max_heartbeat_timeout = client._connection.heartbeat_timeout - - client._connection._update_references(ws) - - log.info('Created websocket connected to %s', gateway) - - # poll event for OP Hello - yield from ws.poll_event() - - if not resume: - yield from ws.identify() - return ws - - yield from ws.resume() - try: - yield from ws.ensure_open() - except websockets.exceptions.ConnectionClosed: - # ws got closed so let's just do a regular IDENTIFY connect. - log.info('RESUME failed (the websocket decided to close) for Shard ID %s. Retrying.', shard_id) - return (yield from cls.from_client(client, shard_id=shard_id)) - else: - return ws - - def wait_for(self, event, predicate, result=None): - """Waits for a DISPATCH'd event that meets the predicate. - - Parameters - ----------- - event : str - The event name in all upper case to wait for. - predicate - A function that takes a data parameter to check for event - properties. The data parameter is the 'd' key in the JSON message. - result - A function that takes the same data parameter and executes to send - the result to the future. If None, returns the data. - - Returns - -------- - asyncio.Future - A future to wait for. - """ - - future = compat.create_future(self.loop) - entry = EventListener(event=event, predicate=predicate, result=result, future=future) - self._dispatch_listeners.append(entry) - return future - - @asyncio.coroutine - def identify(self): - """Sends the IDENTIFY packet.""" - payload = { - 'op': self.IDENTIFY, - 'd': { - 'token': self.token, - 'properties': { - '$os': sys.platform, - '$browser': 'discord.py', - '$device': 'discord.py', - '$referrer': '', - '$referring_domain': '' - }, - 'compress': True, - 'large_threshold': 250, - 'v': 3 - } - } - - if not self._connection.is_bot: - payload['d']['synced_guilds'] = [] - - if self.shard_id is not None and self.shard_count is not None: - payload['d']['shard'] = [self.shard_id, self.shard_count] - - state = self._connection - if state._game is not None or state._status is not None: - payload['d']['presence'] = { - 'status': state._status, - 'game': state._game, - 'since': 0, - 'afk': False - } - - yield from self.send_as_json(payload) - log.info('Shard ID %s has sent the IDENTIFY payload.', self.shard_id) - - @asyncio.coroutine - def resume(self): - """Sends the RESUME packet.""" - payload = { - 'op': self.RESUME, - 'd': { - 'seq': self.sequence, - 'session_id': self.session_id, - 'token': self.token - } - } - - yield from self.send_as_json(payload) - log.info('Shard ID %s has sent the RESUME payload.', self.shard_id) - - @asyncio.coroutine - def received_message(self, msg): - self._dispatch('socket_raw_receive', msg) - - if isinstance(msg, bytes): - msg = zlib.decompress(msg, 15, 10490000) # This is 10 MiB - msg = msg.decode('utf-8') - - msg = json.loads(msg) - - log.debug('For Shard ID %s: WebSocket Event: %s', self.shard_id, msg) - self._dispatch('socket_response', msg) - - op = msg.get('op') - data = msg.get('d') - seq = msg.get('s') - if seq is not None: - self.sequence = seq - - if op == self.RECONNECT: - # "reconnect" can only be handled by the Client - # so we terminate our connection and raise an - # internal exception signalling to reconnect. - log.info('Received RECONNECT opcode.') - yield from self.close() - raise ResumeWebSocket(self.shard_id) - - if op == self.HEARTBEAT_ACK: - self._keep_alive.ack() - return - - if op == self.HEARTBEAT: - beat = self._keep_alive.get_payload() - yield from self.send_as_json(beat) - return - - if op == self.HELLO: - interval = data['heartbeat_interval'] / 1000.0 - self._keep_alive = KeepAliveHandler(ws=self, interval=interval, shard_id=self.shard_id) - # send a heartbeat immediately - yield from self.send_as_json(self._keep_alive.get_payload()) - self._keep_alive.start() - return - - if op == self.INVALIDATE_SESSION: - if data == True: - yield from asyncio.sleep(5.0, loop=self.loop) - yield from self.close() - raise ResumeWebSocket(self.shard_id) - - self.sequence = None - self.session_id = None - log.info('Shard ID %s session has been invalidated.' % self.shard_id) - yield from self.identify() - return - - if op != self.DISPATCH: - log.warning('Unknown OP code %s.', op) - return - - event = msg.get('t') - - if event == 'READY': - self._trace = trace = data.get('_trace', []) - self.sequence = msg['s'] - self.session_id = data['session_id'] - log.info('Shard ID %s has connected to Gateway: %s (Session ID: %s).', - self.shard_id, ', '.join(trace), self.session_id) - - if event == 'RESUMED': - self._trace = trace = data.get('_trace', []) - log.info('Shard ID %s has successfully RESUMED session %s under trace %s.', - self.shard_id, self.session_id, ', '.join(trace)) - - parser = 'parse_' + event.lower() - - try: - func = getattr(self._connection, parser) - except AttributeError: - log.warning('Unknown event %s.', event) - else: - func(data) - - # remove the dispatched listeners - removed = [] - for index, entry in enumerate(self._dispatch_listeners): - if entry.event != event: - continue - - future = entry.future - if future.cancelled(): - removed.append(index) - continue - - try: - valid = entry.predicate(data) - except Exception as e: - future.set_exception(e) - removed.append(index) - else: - if valid: - ret = data if entry.result is None else entry.result(data) - future.set_result(ret) - removed.append(index) - - for index in reversed(removed): - del self._dispatch_listeners[index] - - @property - def latency(self): - """float: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds.""" - heartbeat = self._keep_alive - return float('inf') if heartbeat is None else heartbeat._last_ack - heartbeat._last_send - - def _can_handle_close(self, code): - return code not in (1000, 4004, 4010, 4011) - - @asyncio.coroutine - def poll_event(self): - """Polls for a DISPATCH event and handles the general gateway loop. - - Raises - ------ - ConnectionClosed - The websocket connection was terminated for unhandled reasons. - """ - try: - msg = yield from self.recv() - yield from self.received_message(msg) - except websockets.exceptions.ConnectionClosed as e: - if self._can_handle_close(e.code): - log.info('Websocket closed with %s (%s), attempting a reconnect.', e.code, e.reason) - raise ResumeWebSocket(self.shard_id) from e - else: - log.info('Websocket closed with %s (%s), cannot reconnect.', e.code, e.reason) - raise ConnectionClosed(e, shard_id=self.shard_id) from e - - @asyncio.coroutine - def send(self, data): - self._dispatch('socket_raw_send', data) - yield from super().send(data) - - @asyncio.coroutine - def send_as_json(self, data): - try: - yield from super().send(utils.to_json(data)) - except websockets.exceptions.ConnectionClosed as e: - if not self._can_handle_close(e.code): - raise ConnectionClosed(e, shard_id=self.shard_id) from e - - @asyncio.coroutine - def change_presence(self, *, game=None, status=None, afk=False, since=0.0): - if game is not None and not isinstance(game, Game): - raise InvalidArgument('game must be of type Game or None') - - if status == 'idle': - since = int(time.time() * 1000) - - sent_game = dict(game) if game else None - - payload = { - 'op': self.PRESENCE, - 'd': { - 'game': sent_game, - 'afk': afk, - 'since': since, - 'status': status - } - } - - sent = utils.to_json(payload) - log.debug('Sending "%s" to change status', sent) - yield from self.send(sent) - - @asyncio.coroutine - def request_sync(self, guild_ids): - payload = { - 'op': self.GUILD_SYNC, - 'd': list(guild_ids) - } - yield from self.send_as_json(payload) - - @asyncio.coroutine - def voice_state(self, guild_id, channel_id, self_mute=False, self_deaf=False): - payload = { - 'op': self.VOICE_STATE, - 'd': { - 'guild_id': guild_id, - 'channel_id': channel_id, - 'self_mute': self_mute, - 'self_deaf': self_deaf - } - } - - log.debug('Updating our voice state to %s.', payload) - yield from self.send_as_json(payload) - - @asyncio.coroutine - def close_connection(self, force=False): - if self._keep_alive: - self._keep_alive.stop() - - yield from super().close_connection(force=force) - -class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): - """Implements the websocket protocol for handling voice connections. - - Attributes - ----------- - IDENTIFY - Send only. Starts a new voice session. - SELECT_PROTOCOL - Send only. Tells discord what encryption mode and how to connect for voice. - READY - Receive only. Tells the websocket that the initial connection has completed. - HEARTBEAT - Send only. Keeps your websocket connection alive. - SESSION_DESCRIPTION - Receive only. Gives you the secret key required for voice. - SPEAKING - Send only. Notifies the client if you are currently speaking. - HEARTBEAT_ACK - Receive only. Tells you your heartbeat has been acknowledged. - RESUME - Sent only. Tells the client to resume its session. - HELLO - Receive only. Tells you that your websocket connection was acknowledged. - INVALIDATE_SESSION - Sent only. Tells you that your RESUME request has failed and to re-IDENTIFY. - """ - - IDENTIFY = 0 - SELECT_PROTOCOL = 1 - READY = 2 - HEARTBEAT = 3 - SESSION_DESCRIPTION = 4 - SPEAKING = 5 - HEARTBEAT_ACK = 6 - RESUME = 7 - HELLO = 8 - INVALIDATE_SESSION = 9 - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.max_size = None - self._keep_alive = None - - @asyncio.coroutine - def send_as_json(self, data): - log.debug('Sending voice websocket frame: %s.', data) - yield from self.send(utils.to_json(data)) - - @asyncio.coroutine - def resume(self): - state = self._connection - payload = { - 'op': self.RESUME, - 'd': { - 'token': state.token, - 'server_id': str(state.server_id), - 'session_id': state.session_id - } - } - yield from self.send_as_json(payload) - - @asyncio.coroutine - def identify(self): - state = self._connection - payload = { - 'op': self.IDENTIFY, - 'd': { - 'server_id': str(state.server_id), - 'user_id': str(state.user.id), - 'session_id': state.session_id, - 'token': state.token - } - } - yield from self.send_as_json(payload) - - @classmethod - @asyncio.coroutine - def from_client(cls, client, *, resume=False): - """Creates a voice websocket for the :class:`VoiceClient`.""" - gateway = 'wss://' + client.endpoint + '/?v=3' - ws = yield from websockets.connect(gateway, loop=client.loop, klass=cls) - ws.gateway = gateway - ws._connection = client - ws._max_heartbeat_timeout = 60.0 - - if resume: - yield from ws.resume() - else: - yield from ws.identify() - - return ws - - @asyncio.coroutine - def select_protocol(self, ip, port): - payload = { - 'op': self.SELECT_PROTOCOL, - 'd': { - 'protocol': 'udp', - 'data': { - 'address': ip, - 'port': port, - 'mode': 'xsalsa20_poly1305' - } - } - } - - yield from self.send_as_json(payload) - - @asyncio.coroutine - def speak(self, is_speaking=True): - payload = { - 'op': self.SPEAKING, - 'd': { - 'speaking': is_speaking, - 'delay': 0 - } - } - - yield from self.send_as_json(payload) - - @asyncio.coroutine - def received_message(self, msg): - log.debug('Voice websocket frame received: %s', msg) - op = msg['op'] - data = msg.get('d') - - if op == self.READY: - interval = data['heartbeat_interval'] / 1000.0 - self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=interval) - self._keep_alive.start() - yield from self.initial_connection(data) - elif op == self.HEARTBEAT_ACK: - self._keep_alive.ack() - elif op == self.INVALIDATE_SESSION: - log.info('Voice RESUME failed.') - yield from self.identify() - elif op == self.SESSION_DESCRIPTION: - yield from self.load_secret_key(data) - - @asyncio.coroutine - def initial_connection(self, data): - state = self._connection - state.ssrc = data['ssrc'] - state.voice_port = data['port'] - - packet = bytearray(70) - struct.pack_into('>I', packet, 0, state.ssrc) - state.socket.sendto(packet, (state.endpoint_ip, state.voice_port)) - recv = yield from self.loop.sock_recv(state.socket, 70) - log.debug('received packet in initial_connection: %s', recv) - - # the ip is ascii starting at the 4th byte and ending at the first null - ip_start = 4 - ip_end = recv.index(0, ip_start) - state.ip = recv[ip_start:ip_end].decode('ascii') - - # the port is a little endian unsigned short in the last two bytes - # yes, this is different endianness from everything else - state.port = struct.unpack_from(''.format(self) - - def _update_voice_state(self, data, channel_id): - user_id = int(data['user_id']) - channel = self.get_channel(channel_id) - try: - # check if we should remove the voice state from cache - if channel is None: - after = self._voice_states.pop(user_id) - else: - after = self._voice_states[user_id] - - before = copy.copy(after) - after._update(data, channel) - except KeyError: - # if we're here then we're getting added into the cache - after = VoiceState(data=data, channel=channel) - before = VoiceState(data=data, channel=None) - self._voice_states[user_id] = after - - member = self.get_member(user_id) - return member, before, after - - def _add_role(self, role): - # roles get added to the bottom (position 1, pos 0 is @everyone) - # so since self.roles has the @everyone role, we can't increment - # its position because it's stuck at position 0. Luckily x += False - # is equivalent to adding 0. So we cast the position to a bool and - # increment it. - for r in self.roles: - r.position += bool(r.position) - - self.roles.append(role) - - def _remove_role(self, role): - # this raises ValueError if it fails.. - self.roles.remove(role) - - # since it didn't, we can change the positions now - # basically the same as above except we only decrement - # the position if we're above the role we deleted. - for r in self.roles: - r.position -= r.position > role.position - - def _from_data(self, guild): - # according to Stan, this is always available even if the guild is unavailable - # I don't have this guarantee when someone updates the guild. - member_count = guild.get('member_count', None) - if member_count: - self._member_count = member_count - - self.name = guild.get('name') - self.region = try_enum(VoiceRegion, guild.get('region')) - self.verification_level = try_enum(VerificationLevel, guild.get('verification_level')) - self.explicit_content_filter = try_enum(ContentFilter, guild.get('explicit_content_filter', 0)) - self.afk_timeout = guild.get('afk_timeout') - self.icon = guild.get('icon') - self.unavailable = guild.get('unavailable', False) - self.id = int(guild['id']) - self.roles = [Role(guild=self, data=r, state=self._state) for r in guild.get('roles', [])] - self.mfa_level = guild.get('mfa_level') - self.emojis = tuple(map(lambda d: self._state.store_emoji(self, d), guild.get('emojis', []))) - self.features = guild.get('features', []) - self.splash = guild.get('splash') - self._system_channel_id = guild.get('system_channel_id') - - for mdata in guild.get('members', []): - member = Member(data=mdata, guild=self, state=self._state) - self._add_member(member) - - self._sync(guild) - self._large = None if member_count is None else self._member_count >= 250 - - self.owner_id = utils._get_as_snowflake(guild, 'owner_id') - self.afk_channel = self.get_channel(utils._get_as_snowflake(guild, 'afk_channel_id')) - - for obj in guild.get('voice_states', []): - self._update_voice_state(obj, int(obj['channel_id'])) - - def _sync(self, data): - try: - self._large = data['large'] - except KeyError: - pass - - for presence in data.get('presences', []): - user_id = int(presence['user']['id']) - member = self.get_member(user_id) - if member is not None: - member.status = try_enum(Status, presence['status']) - game = presence.get('game', {}) - member.game = Game(**game) if game else None - - if 'channels' in data: - channels = data['channels'] - for c in channels: - if c['type'] == ChannelType.text.value: - self._add_channel(TextChannel(guild=self, data=c, state=self._state)) - elif c['type'] == ChannelType.voice.value: - self._add_channel(VoiceChannel(guild=self, data=c, state=self._state)) - elif c['type'] == ChannelType.category.value: - self._add_channel(CategoryChannel(guild=self, data=c, state=self._state)) - - - @property - def channels(self): - """List[:class:`abc.GuildChannel`]: A list of channels that belongs to this guild.""" - return list(self._channels.values()) - - @property - def large(self): - """bool: Indicates if the guild is a 'large' guild. - - A large guild is defined as having more than ``large_threshold`` count - members, which for this library is set to the maximum of 250. - """ - if self._large is None: - try: - return self._member_count >= 250 - except AttributeError: - return len(self._members) >= 250 - return self._large - - @property - def voice_channels(self): - """List[:class:`VoiceChannel`]: A list of voice channels that belongs to this guild. - - This is sorted by the position and are in UI order from top to bottom. - """ - r = [ch for ch in self._channels.values() if isinstance(ch, VoiceChannel)] - r.sort(key=lambda c: (c.position, c.id)) - return r - - @property - def me(self): - """Similar to :attr:`Client.user` except an instance of :class:`Member`. - This is essentially used to get the member version of yourself. - """ - self_id = self._state.user.id - return self.get_member(self_id) - - @property - def voice_client(self): - """Returns the :class:`VoiceClient` associated with this guild, if any.""" - return self._state._get_voice_client(self.id) - - @property - def text_channels(self): - """List[:class:`TextChannel`]: A list of text channels that belongs to this guild. - - This is sorted by the position and are in UI order from top to bottom. - """ - r = [ch for ch in self._channels.values() if isinstance(ch, TextChannel)] - r.sort(key=lambda c: (c.position, c.id)) - return r - - @property - def categories(self): - """List[:class:`CategoryChannel`]: A list of categories that belongs to this guild. - - This is sorted by the position and are in UI order from top to bottom. - """ - r = [ch for ch in self._channels.values() if isinstance(ch, CategoryChannel)] - r.sort(key=lambda c: (c.position, c.id)) - return r - - def by_category(self): - """Returns every :class:`CategoryChannel` and their associated channels. - - These channels and categories are sorted in the official Discord UI order. - - If the channels do not have a category, then the first element of the tuple is - ``None``. - - Returns - -------- - List[Tuple[Optional[:class:`CategoryChannel`], List[:class:`abc.GuildChannel`]]]: - The categories and their associated channels. - """ - grouped = defaultdict(list) - for channel in self._channels.values(): - if isinstance(channel, CategoryChannel): - continue - - grouped[channel.category_id].append(channel) - - def key(t): - k, v = t - return ((k.position, k.id) if k else (-1, -1), v) - - _get = self._channels.get - as_list = [(_get(k), v) for k, v in grouped.items()] - as_list.sort(key=key) - for _, channels in as_list: - channels.sort(key=lambda c: (c.position, c.id)) - return as_list - - def get_channel(self, channel_id): - """Returns a :class:`abc.GuildChannel` with the given ID. If not found, returns None.""" - return self._channels.get(channel_id) - - @property - def system_channel(self): - """Optional[:class:`TextChannel`]: Returns the guild's channel used for system messages. - - Currently this is only for new member joins. If no channel is set, then this returns ``None``. - """ - channel_id = self._system_channel_id - return channel_id and self._channels.get(channel_id) - - @property - def members(self): - """List[:class:`Member`]: A list of members that belongs to this guild.""" - return list(self._members.values()) - - def get_member(self, user_id): - """Returns a :class:`Member` with the given ID. If not found, returns None.""" - return self._members.get(user_id) - - @utils.cached_slot_property('_default_role') - def default_role(self): - """Gets the @everyone role that all members have by default.""" - return utils.find(lambda r: r.is_default(), self.roles) - - @property - def owner(self): - """:class:`Member`: The member that owns the guild.""" - return self.get_member(self.owner_id) - - @property - def icon_url(self): - """Returns the URL version of the guild's icon. Returns an empty string if it has no icon.""" - return self.icon_url_as() - - def icon_url_as(self, *, format='webp', size=1024): - """Returns a friendly URL version of the guild's icon. Returns and empty string if it has no icon. - - The format must be one of 'webp', 'jpeg', 'jpg', or 'png'. The - size must be a power of 2 between 16 and 1024. - - Parameters - ----------- - format: str - The format to attempt to convert the icon to. - size: int - The size of the image to display. - - Returns - -------- - str - The resulting CDN URL. - - Raises - ------ - InvalidArgument - Bad image format passed to ``format`` or invalid ``size``. - """ - if not valid_icon_size(size): - raise InvalidArgument("size must be a power of 2 between 16 and 1024") - if format not in VALID_ICON_FORMATS: - raise InvalidArgument("format must be one of {}".format(VALID_ICON_FORMATS)) - - if self.icon is None: - return '' - - return 'https://cdn.discordapp.com/icons/{0.id}/{0.icon}.{1}?size={2}'.format(self, format, size) - - @property - def splash_url(self): - """Returns the URL version of the guild's invite splash. Returns an empty string if it has no splash.""" - if self.splash is None: - return '' - return 'https://cdn.discordapp.com/splashes/{0.id}/{0.splash}.jpg?size=2048'.format(self) - - @property - def member_count(self): - """Returns the true member count regardless of it being loaded fully or not.""" - return self._member_count - - @property - def chunked(self): - """Returns a boolean indicating if the guild is "chunked". - - A chunked guild means that :attr:`member_count` is equal to the - number of members stored in the internal :attr:`members` cache. - - If this value returns ``False``, then you should request for - offline members. - """ - count = getattr(self, '_member_count', None) - if count is None: - return False - return count == len(self._members) - - @property - def shard_id(self): - """Returns the shard ID for this guild if applicable.""" - count = self._state.shard_count - if count is None: - return None - return (self.id >> 22) % count - - @property - def created_at(self): - """Returns the guild's creation time in UTC.""" - return utils.snowflake_time(self.id) - - @property - def role_hierarchy(self): - """Returns the guild's roles in the order of the hierarchy. - - The first element of this list will be the highest role in the - hierarchy. - """ - return sorted(self.roles, reverse=True) - - def get_member_named(self, name): - """Returns the first member found that matches the name provided. - - The name can have an optional discriminator argument, e.g. "Jake#0001" - or "Jake" will both do the lookup. However the former will give a more - precise result. Note that the discriminator must have all 4 digits - for this to work. - - If a nickname is passed, then it is looked up via the nickname. Note - however, that a nickname + discriminator combo will not lookup the nickname - but rather the username + discriminator combo due to nickname + discriminator - not being unique. - - If no member is found, ``None`` is returned. - - Parameters - ----------- - name: str - The name of the member to lookup with an optional discriminator. - - Returns - -------- - :class:`Member` - The member in this guild with the associated name. If not found - then ``None`` is returned. - """ - - result = None - members = self.members - if len(name) > 5 and name[-5] == '#': - # The 5 length is checking to see if #0000 is in the string, - # as a#0000 has a length of 6, the minimum for a potential - # discriminator lookup. - potential_discriminator = name[-4:] - - # do the actual lookup and return if found - # if it isn't found then we'll do a full name lookup below. - result = utils.get(members, name=name[:-5], discriminator=potential_discriminator) - if result is not None: - return result - - def pred(m): - return m.nick == name or m.name == name - - return utils.find(pred, members) - - def _create_channel(self, name, overwrites, channel_type, reason): - if overwrites is None: - overwrites = {} - elif not isinstance(overwrites, dict): - raise InvalidArgument('overwrites parameter expects a dict.') - - perms = [] - for target, perm in overwrites.items(): - if not isinstance(perm, PermissionOverwrite): - raise InvalidArgument('Expected PermissionOverwrite received {0.__name__}'.format(type(perm))) - - allow, deny = perm.pair() - payload = { - 'allow': allow.value, - 'deny': deny.value, - 'id': target.id - } - - if isinstance(target, Role): - payload['type'] = 'role' - else: - payload['type'] = 'member' - - perms.append(payload) - - return self._state.http.create_channel(self.id, name, channel_type.value, permission_overwrites=perms, reason=reason) - - @asyncio.coroutine - def create_text_channel(self, name, *, overwrites=None, reason=None): - """|coro| - - Creates a :class:`TextChannel` for the guild. - - Note that you need the proper permissions to create the channel. - - The ``overwrites`` parameter can be used to create a 'secret' - channel upon creation. This parameter expects a `dict` of - overwrites with the target (either a :class:`Member` or a :class:`Role`) - as the key and a :class:`PermissionOverwrite` as the value. - - Examples - ---------- - - Creating a basic channel: - - .. code-block:: python3 - - channel = await guild.create_text_channel('cool-channel') - - Creating a "secret" channel: - - .. code-block:: python3 - - overwrites = { - guild.default_role: discord.PermissionOverwrite(read_messages=False), - guild.me: discord.PermissionOverwrite(read_messages=True) - } - - channel = await guild.create_text_channel('secret', overwrites=overwrites) - - Parameters - ----------- - name: str - The channel's name. - overwrites - A `dict` of target (either a role or a member) to - :class:`PermissionOverwrite` to apply upon creation of a channel. - Useful for creating secret channels. - reason: Optional[str] - The reason for creating this channel. Shows up on the audit log. - - Raises - ------- - Forbidden - You do not have the proper permissions to create this channel. - HTTPException - Creating the channel failed. - InvalidArgument - The permission overwrite information is not in proper form. - - Returns - ------- - :class:`TextChannel` - The channel that was just created. - """ - data = yield from self._create_channel(name, overwrites, ChannelType.text, reason=reason) - return TextChannel(state=self._state, guild=self, data=data) - - @asyncio.coroutine - def create_voice_channel(self, name, *, overwrites=None, reason=None): - """|coro| - - Same as :meth:`create_text_channel` except makes a :class:`VoiceChannel` instead. - """ - data = yield from self._create_channel(name, overwrites, ChannelType.voice, reason=reason) - return VoiceChannel(state=self._state, guild=self, data=data) - - @asyncio.coroutine - def create_category(self, name, *, overwrites=None, reason=None): - """|coro| - - Same as :meth:`create_text_channel` except makes a :class:`CategoryChannel` instead. - """ - data = yield from self._create_channel(name, overwrites, ChannelType.category, reason=reason) - return CategoryChannel(state=self._state, guild=self, data=data) - - create_category_channel = create_category - - @asyncio.coroutine - def leave(self): - """|coro| - - Leaves the guild. - - Note - -------- - You cannot leave the guild that you own, you must delete it instead - via :meth:`delete`. - - Raises - -------- - HTTPException - Leaving the guild failed. - """ - yield from self._state.http.leave_guild(self.id) - - @asyncio.coroutine - def delete(self): - """|coro| - - Deletes the guild. You must be the guild owner to delete the - guild. - - Raises - -------- - HTTPException - Deleting the guild failed. - Forbidden - You do not have permissions to delete the guild. - """ - - yield from self._state.http.delete_guild(self.id) - - @asyncio.coroutine - def edit(self, *, reason=None, **fields): - """|coro| - - Edits the guild. - - You must have the :attr:`~Permissions.manage_guild` permission - to edit the guild. - - Parameters - ---------- - name: str - The new name of the guild. - icon: bytes - A *bytes-like* object representing the icon. Only PNG/JPEG supported. - Could be ``None`` to denote removal of the icon. - splash: bytes - A *bytes-like* object representing the invite splash. - Only PNG/JPEG supported. Could be ``None`` to denote removing the - splash. Only available for partnered guilds with ``INVITE_SPLASH`` - feature. - region: :class:`VoiceRegion` - The new region for the guild's voice communication. - afk_channel: Optional[:class:`VoiceChannel`] - The new channel that is the AFK channel. Could be ``None`` for no AFK channel. - afk_timeout: int - The number of seconds until someone is moved to the AFK channel. - owner: :class:`Member` - The new owner of the guild to transfer ownership to. Note that you must - be owner of the guild to do this. - verification_level: :class:`VerificationLevel` - The new verification level for the guild. - vanity_code: str - The new vanity code for the guild. - system_channel: Optional[:class:`TextChannel`] - The new channel that is used for the system channel. Could be ``None`` for no system channel. - reason: Optional[str] - The reason for editing this guild. Shows up on the audit log. - - Raises - ------- - Forbidden - You do not have permissions to edit the guild. - HTTPException - Editing the guild failed. - InvalidArgument - The image format passed in to ``icon`` is invalid. It must be - PNG or JPG. This is also raised if you are not the owner of the - guild and request an ownership transfer. - """ - - http = self._state.http - try: - icon_bytes = fields['icon'] - except KeyError: - icon = self.icon - else: - if icon_bytes is not None: - icon = utils._bytes_to_base64_data(icon_bytes) - else: - icon = None - - try: - vanity_code = fields['vanity_code'] - except KeyError: - pass - else: - yield from http.change_vanity_code(self.id, vanity_code, reason=reason) - - try: - splash_bytes = fields['splash'] - except KeyError: - splash = self.splash - else: - if splash_bytes is not None: - splash = utils._bytes_to_base64_data(splash_bytes) - else: - splash = None - - fields['icon'] = icon - fields['splash'] = splash - - try: - afk_channel = fields.pop('afk_channel') - except KeyError: - pass - else: - if afk_channel is None: - fields['afk_channel_id'] = afk_channel - else: - fields['afk_channel_id'] = afk_channel.id - - try: - system_channel = fields.pop('system_channel') - except KeyError: - pass - else: - if system_channel is None: - fields['system_channel_id'] = system_channel - else: - fields['system_channel_id'] = system_channel.id - - if 'owner' in fields: - if self.owner != self.me: - raise InvalidArgument('To transfer ownership you must be the owner of the guild.') - - fields['owner_id'] = fields['owner'].id - - if 'region' in fields: - fields['region'] = str(fields['region']) - - level = fields.get('verification_level', self.verification_level) - if not isinstance(level, VerificationLevel): - raise InvalidArgument('verification_level field must of type VerificationLevel') - - fields['verification_level'] = level.value - - yield from http.edit_guild(self.id, reason=reason, **fields) - - - @asyncio.coroutine - def bans(self): - """|coro| - - Retrieves all the users that are banned from the guild. - - This coroutine returns a list of BanEntry objects. Which is a - namedtuple with a ``user`` field to denote the :class:`User` - that got banned along with a ``reason`` field specifying - why the user was banned that could be set to ``None``. - - You must have :attr:`~Permissions.ban_members` permission - to get this information. - - Raises - ------- - Forbidden - You do not have proper permissions to get the information. - HTTPException - An error occurred while fetching the information. - - Returns - -------- - List[BanEntry] - A list of BanEntry objects. - """ - - data = yield from self._state.http.get_bans(self.id) - return [BanEntry(user=User(state=self._state, data=e['user']), - reason=e['reason']) - for e in data] - - @asyncio.coroutine - def prune_members(self, *, days, reason=None): - """|coro| - - Prunes the guild from its inactive members. - - The inactive members are denoted if they have not logged on in - ``days`` number of days and they have no roles. - - You must have the :attr:`~Permissions.kick_members` permission - to use this. - - To check how many members you would prune without actually pruning, - see the :meth:`estimate_pruned_members` function. - - Parameters - ----------- - days: int - The number of days before counting as inactive. - reason: Optional[str] - The reason for doing this action. Shows up on the audit log. - - Raises - ------- - Forbidden - You do not have permissions to prune members. - HTTPException - An error occurred while pruning members. - InvalidArgument - An integer was not passed for ``days``. - - Returns - --------- - int - The number of members pruned. - """ - - if not isinstance(days, int): - raise InvalidArgument('Expected int for ``days``, received {0.__class__.__name__} instead.'.format(days)) - - data = yield from self._state.http.prune_members(self.id, days, reason=reason) - return data['pruned'] - - @asyncio.coroutine - def webhooks(self): - """|coro| - - Gets the list of webhooks from this guild. - - Requires :attr:`~.Permissions.manage_webhooks` permissions. - - Raises - ------- - Forbidden - You don't have permissions to get the webhooks. - - Returns - -------- - List[:class:`Webhook`] - The webhooks for this guild. - """ - - data = yield from self._state.http.guild_webhooks(self.id) - return [Webhook.from_state(d, state=self._state) for d in data] - - @asyncio.coroutine - def estimate_pruned_members(self, *, days): - """|coro| - - Similar to :meth:`prune_members` except instead of actually - pruning members, it returns how many members it would prune - from the guild had it been called. - - Parameters - ----------- - days: int - The number of days before counting as inactive. - - Raises - ------- - Forbidden - You do not have permissions to prune members. - HTTPException - An error occurred while fetching the prune members estimate. - InvalidArgument - An integer was not passed for ``days``. - - Returns - --------- - int - The number of members estimated to be pruned. - """ - - if not isinstance(days, int): - raise InvalidArgument('Expected int for ``days``, received {0.__class__.__name__} instead.'.format(days)) - - data = yield from self._state.http.estimate_pruned_members(self.id, days) - return data['pruned'] - - @asyncio.coroutine - def invites(self): - """|coro| - - Returns a list of all active instant invites from the guild. - - You must have :attr:`~Permissions.manage_guild` to get this information. - - Raises - ------- - Forbidden - You do not have proper permissions to get the information. - HTTPException - An error occurred while fetching the information. - - Returns - ------- - List[:class:`Invite`] - The list of invites that are currently active. - """ - - data = yield from self._state.http.invites_from(self.id) - result = [] - for invite in data: - channel = self.get_channel(int(invite['channel']['id'])) - invite['channel'] = channel - invite['guild'] = self - result.append(Invite(state=self._state, data=invite)) - - return result - - @asyncio.coroutine - def create_custom_emoji(self, *, name, image, reason=None): - """|coro| - - Creates a custom :class:`Emoji` for the guild. - - This endpoint is only allowed for user bots or white listed - bots. If this is done by a user bot then this is a local - emoji that can only be used inside the guild. If done by - a whitelisted bot, then this emoji is "global". - - There is currently a limit of 50 local emotes per guild. - - Parameters - ----------- - name: str - The emoji name. Must be at least 2 characters. - image: bytes - The *bytes-like* object representing the image data to use. - Only JPG and PNG images are supported. - reason: Optional[str] - The reason for creating this emoji. Shows up on the audit log. - - Returns - -------- - :class:`Emoji` - The created emoji. - - Raises - ------- - Forbidden - You are not allowed to create emojis. - HTTPException - An error occurred creating an emoji. - """ - - img = utils._bytes_to_base64_data(image) - data = yield from self._state.http.create_custom_emoji(self.id, name, img, reason=reason) - return self._state.store_emoji(self, data) - - @asyncio.coroutine - def create_role(self, *, reason=None, **fields): - """|coro| - - Creates a :class:`Role` for the guild. - - All fields are optional. - - Parameters - ----------- - name: str - The role name. Defaults to 'new role'. - permissions: :class:`Permissions` - The permissions to have. Defaults to no permissions. - colour: :class:`Colour` - The colour for the role. Defaults to :meth:`Colour.default`. - This is aliased to ``color`` as well. - hoist: bool - Indicates if the role should be shown separately in the member list. - Defaults to False. - mentionable: bool - Indicates if the role should be mentionable by others. - Defaults to False. - reason: Optional[str] - The reason for creating this role. Shows up on the audit log. - - Returns - -------- - :class:`Role` - The newly created role. - - Raises - ------- - Forbidden - You do not have permissions to change the role. - HTTPException - Editing the role failed. - InvalidArgument - An invalid keyword argument was given. - """ - - try: - perms = fields.pop('permissions') - except KeyError: - fields['permissions'] = 0 - else: - fields['permissions'] = perms.value - - try: - colour = fields.pop('colour') - except KeyError: - colour = fields.get('color', Colour.default()) - finally: - fields['color'] = colour.value - - valid_keys = ('name', 'permissions', 'color', 'hoist', 'mentionable') - for key in fields: - if key not in valid_keys: - raise InvalidArgument('%r is not a valid field.' % key) - - data = yield from self._state.http.create_role(self.id, reason=reason, **fields) - role = Role(guild=self, data=data, state=self._state) - - # TODO: add to cache - return role - - @asyncio.coroutine - def kick(self, user, *, reason=None): - """|coro| - - Kicks a user from the guild. - - The user must meet the :class:`abc.Snowflake` abc. - - You must have :attr:`Permissions.kick_members` permissions to - do this. - - Parameters - ----------- - user: :class:`abc.Snowflake` - The user to kick from their guild. - reason: Optional[str] - The reason the user got kicked. - - Raises - ------- - Forbidden - You do not have the proper permissions to kick. - HTTPException - Kicking failed. - """ - yield from self._state.http.kick(user.id, self.id, reason=reason) - - @asyncio.coroutine - def ban(self, user, *, reason=None, delete_message_days=1): - """|coro| - - Bans a user from the guild. - - The user must meet the :class:`abc.Snowflake` abc. - - You must have :attr:`Permissions.ban_members` permissions to - do this. - - Parameters - ----------- - user: :class:`abc.Snowflake` - The user to ban from their guild. - delete_message_days: int - The number of days worth of messages to delete from the user - in the guild. The minimum is 0 and the maximum is 7. - reason: Optional[str] - The reason the user got banned. - - Raises - ------- - Forbidden - You do not have the proper permissions to ban. - HTTPException - Banning failed. - """ - yield from self._state.http.ban(user.id, self.id, delete_message_days, reason=reason) - - @asyncio.coroutine - def unban(self, user, *, reason=None): - """|coro| - - Unbans a user from the guild. - - The user must meet the :class:`abc.Snowflake` abc. - - You must have :attr:`Permissions.ban_members` permissions to - do this. - - Parameters - ----------- - user: :class:`abc.Snowflake` - The user to unban. - reason: Optional[str] - The reason for doing this action. Shows up on the audit log. - - Raises - ------- - Forbidden - You do not have the proper permissions to unban. - HTTPException - Unbanning failed. - """ - yield from self._state.http.unban(user.id, self.id, reason=reason) - - @asyncio.coroutine - def vanity_invite(self): - """|coro| - - Returns the guild's special vanity invite. - - The guild must be partnered, i.e. have 'VANITY_URL' in - :attr:`~Guild.features`. - - You must have :attr:`Permissions.manage_guild` to use this as well. - - Returns - -------- - :class:`Invite` - The special vanity invite. - - Raises - ------- - Forbidden - You do not have the proper permissions to get this. - HTTPException - Retrieving the vanity invite failed. - """ - - # we start with { code: abc } - payload = yield from self._state.http.get_vanity_code(self.id) - - # get the vanity URL channel since default channels aren't - # reliable or a thing anymore - data = yield from self._state.http.get_invite(payload['code']) - - payload['guild'] = self - payload['channel'] = self.get_channel(int(data['channel']['id'])) - payload['revoked'] = False - payload['temporary'] = False - payload['max_uses'] = 0 - payload['max_age'] = 0 - return Invite(state=self._state, data=payload) - - def ack(self): - """|coro| - - Marks every message in this guild as read. - - The user must not be a bot user. - - Raises - ------- - HTTPException - Acking failed. - ClientException - You must not be a bot user. - """ - - state = self._state - if state.is_bot: - raise ClientException('Must not be a bot account to ack messages.') - return state.http.ack_guild(self.id) - - def audit_logs(self, *, limit=100, before=None, after=None, reverse=None, user=None, action=None): - """Return an :class:`AsyncIterator` that enables receiving the guild's audit logs. - - You must have :attr:`Permissions.view_audit_logs` permission to use this. - - Parameters - ----------- - limit: Optional[int] - The number of entries to retrieve. If ``None`` retrieve all entries. - before: Union[:class:`abc.Snowflake`, datetime] - Retrieve entries before this date or entry. - If a date is provided it must be a timezone-naive datetime representing UTC time. - after: Union[:class:`abc.Snowflake`, datetime] - Retrieve entries after this date or entry. - If a date is provided it must be a timezone-naive datetime representing UTC time. - reverse: bool - If set to true, return entries in oldest->newest order. If unspecified, - this defaults to ``False`` for most cases. However if passing in a - ``after`` parameter then this is set to ``True``. This avoids getting entries - out of order in the ``after`` case. - user: :class:`abc.Snowflake` - The moderator to filter entries from. - action: :class:`AuditLogAction` - The action to filter with. - - Yields - -------- - :class:`AuditLogEntry` - The audit log entry. - - Raises - ------- - Forbidden - You are not allowed to fetch audit logs - HTTPException - An error occurred while fetching the audit logs. - - Examples - ---------- - - Getting the first 100 entries: :: - - async for entry in guild.audit_logs(limit=100): - print('{0.user} did {0.action} to {0.target}'.format(entry)) - - Getting entries for a specific action: :: - - async for entry in guild.audit_logs(action=discord.AuditLogAction.ban): - print('{0.user} banned {0.target}'.format(entry)) - - Getting entries made by a specific user: :: - - entries = await guild.audit_logs(limit=None, user=guild.me).flatten() - await channel.send('I made {} moderation actions.'.format(len(entries))) - """ - if user: - user = user.id - - if action: - action = action.value - - return AuditLogIterator(self, before=before, after=after, limit=limit, - reverse=reverse, user_id=user, action_type=action) diff --git a/discord.py-rewrite/discord/http.py b/discord.py-rewrite/discord/http.py deleted file mode 100644 index 1c020ee..0000000 --- a/discord.py-rewrite/discord/http.py +++ /dev/null @@ -1,759 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import aiohttp -import asyncio -import json -import sys -import logging -import weakref -import datetime -from urllib.parse import quote as _uriquote - -log = logging.getLogger(__name__) - -from .errors import HTTPException, Forbidden, NotFound, LoginFailure, GatewayNotFound -from . import __version__, utils - -@asyncio.coroutine -def json_or_text(response): - text = yield from response.text(encoding='utf-8') - if response.headers['content-type'] == 'application/json': - return json.loads(text) - return text - -class Route: - BASE = 'https://discordapp.com/api/v7' - - def __init__(self, method, path, **parameters): - self.path = path - self.method = method - url = (self.BASE + self.path) - if parameters: - self.url = url.format(**parameters) - else: - self.url = url - - # major parameters: - self.channel_id = parameters.get('channel_id') - self.guild_id = parameters.get('guild_id') - - @property - def bucket(self): - # the bucket is just method + path w/ major parameters - return '{0.method}:{0.channel_id}:{0.guild_id}:{0.path}'.format(self) - -class MaybeUnlock: - def __init__(self, lock): - self.lock = lock - self._unlock = True - - def __enter__(self): - return self - - def defer(self): - self._unlock = False - - def __exit__(self, type, value, traceback): - if self._unlock: - self.lock.release() - -class HTTPClient: - """Represents an HTTP client sending HTTP requests to the Discord API.""" - - SUCCESS_LOG = '{method} {url} has received {text}' - REQUEST_LOG = '{method} {url} with {json} has returned {status}' - - def __init__(self, connector=None, *, proxy=None, proxy_auth=None, loop=None): - self.loop = asyncio.get_event_loop() if loop is None else loop - self.connector = connector - self._session = aiohttp.ClientSession(connector=connector, loop=self.loop) - self._locks = weakref.WeakValueDictionary() - self._global_over = asyncio.Event(loop=self.loop) - self._global_over.set() - self.token = None - self.bot_token = False - self.proxy = proxy - self.proxy_auth = proxy_auth - - user_agent = 'DiscordBot (https://github.com/Rapptz/discord.py {0}) Python/{1[0]}.{1[1]} aiohttp/{2}' - self.user_agent = user_agent.format(__version__, sys.version_info, aiohttp.__version__) - - @asyncio.coroutine - def request(self, route, *, header_bypass_delay=None, **kwargs): - bucket = route.bucket - method = route.method - url = route.url - - lock = self._locks.get(bucket) - if lock is None: - lock = asyncio.Lock(loop=self.loop) - if bucket is not None: - self._locks[bucket] = lock - - # header creation - headers = { - 'User-Agent': self.user_agent, - } - - if self.token is not None: - headers['Authorization'] = 'Bot ' + self.token if self.bot_token else self.token - # some checking if it's a JSON request - if 'json' in kwargs: - headers['Content-Type'] = 'application/json' - kwargs['data'] = utils.to_json(kwargs.pop('json')) - - try: - reason = kwargs.pop('reason') - except KeyError: - pass - else: - if reason: - headers['X-Audit-Log-Reason'] = _uriquote(reason, safe='/ ') - - kwargs['headers'] = headers - - # Proxy support - if self.proxy is not None: - kwargs['proxy'] = self.proxy - if self.proxy_auth is not None: - kwargs['proxy_auth'] = self.proxy_auth - - if not self._global_over.is_set(): - # wait until the global lock is complete - yield from self._global_over.wait() - - yield from lock - with MaybeUnlock(lock) as maybe_lock: - for tries in range(5): - r = yield from self._session.request(method, url, **kwargs) - log.debug('%s %s with %s has returned %s', method, url, kwargs.get('data'), r.status) - try: - # even errors have text involved in them so this is safe to call - data = yield from json_or_text(r) - - # check if we have rate limit header information - remaining = r.headers.get('X-Ratelimit-Remaining') - if remaining == '0' and r.status != 429: - # we've depleted our current bucket - if header_bypass_delay is None: - delta = utils._parse_ratelimit_header(r) - else: - delta = header_bypass_delay - - log.info('A rate limit bucket has been exhausted (bucket: %s, retry: %s).', bucket, delta) - maybe_lock.defer() - self.loop.call_later(delta, lock.release) - - # the request was successful so just return the text/json - if 300 > r.status >= 200: - log.debug('%s %s has received %s', method, url, data) - return data - - # we are being rate limited - if r.status == 429: - fmt = 'We are being rate limited. Retrying in %.2f seconds. Handled under the bucket "%s"' - - # sleep a bit - retry_after = data['retry_after'] / 1000.0 - log.info(fmt, retry_after, bucket) - - # check if it's a global rate limit - is_global = data.get('global', False) - if is_global: - log.info('Global rate limit has been hit. Retrying in %.2f seconds.', retry_after) - self._global_over.clear() - - yield from asyncio.sleep(retry_after, loop=self.loop) - log.debug('Done sleeping for the rate limit. Retrying...') - - # release the global lock now that the - # global rate limit has passed - if is_global: - self._global_over.set() - log.debug('Global rate limit is now over.') - - continue - - # we've received a 500 or 502, unconditional retry - if r.status in {500, 502}: - yield from asyncio.sleep(1 + tries * 2, loop=self.loop) - continue - - # the usual error cases - if r.status == 403: - raise Forbidden(r, data) - elif r.status == 404: - raise NotFound(r, data) - else: - raise HTTPException(r, data) - finally: - # clean-up just in case - yield from r.release() - # We've run out of retries, raise. - raise HTTPException(r, data) - - def get_attachment(self, url): - resp = yield from self._session.get(url) - try: - if resp.status == 200: - return (yield from resp.read()) - elif resp.status == 404: - raise NotFound(resp, 'attachment not found') - elif resp.status == 403: - raise Forbidden(resp, 'cannot retrieve attachment') - else: - raise HTTPException(resp, 'failed to get attachment') - finally: - yield from resp.release() - - # state management - - @asyncio.coroutine - def close(self): - yield from self._session.close() - - def _token(self, token, *, bot=True): - self.token = token - self.bot_token = bot - self._ack_token = None - - # login management - - @asyncio.coroutine - def static_login(self, token, *, bot): - old_token, old_bot = self.token, self.bot_token - self._token(token, bot=bot) - - try: - data = yield from self.request(Route('GET', '/users/@me')) - except HTTPException as e: - self._token(old_token, bot=old_bot) - if e.response.status == 401: - raise LoginFailure('Improper token has been passed.') from e - raise e - - return data - - def logout(self): - return self.request(Route('POST', '/auth/logout')) - - # Group functionality - - def start_group(self, user_id, recipients): - payload = { - 'recipients': recipients - } - - return self.request(Route('POST', '/users/{user_id}/channels', user_id=user_id), json=payload) - - def leave_group(self, channel_id): - return self.request(Route('DELETE', '/channels/{channel_id}', channel_id=channel_id)) - - def add_group_recipient(self, channel_id, user_id): - r = Route('PUT', '/channels/{channel_id}/recipients/{user_id}', channel_id=channel_id, user_id=user_id) - return self.request(r) - - def remove_group_recipient(self, channel_id, user_id): - r = Route('DELETE', '/channels/{channel_id}/recipients/{user_id}', channel_id=channel_id, user_id=user_id) - return self.request(r) - - def edit_group(self, channel_id, **options): - valid_keys = ('name', 'icon') - payload = { - k: v for k, v in options.items() if k in valid_keys - } - - return self.request(Route('PATCH', '/channels/{channel_id}', channel_id=channel_id), json=payload) - - def convert_group(self, channel_id): - return self.request(Route('POST', '/channels/{channel_id}/convert', channel_id=channel_id)) - - # Message management - - def start_private_message(self, user_id): - payload = { - 'recipient_id': user_id - } - - return self.request(Route('POST', '/users/@me/channels'), json=payload) - - def send_message(self, channel_id, content, *, tts=False, embed=None, nonce=None): - r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) - payload = {} - - if content: - payload['content'] = content - - if tts: - payload['tts'] = True - - if embed: - payload['embed'] = embed - - if nonce: - payload['nonce'] = nonce - - return self.request(r, json=payload) - - def send_typing(self, channel_id): - return self.request(Route('POST', '/channels/{channel_id}/typing', channel_id=channel_id)) - - def send_files(self, channel_id, *, files, content=None, tts=False, embed=None, nonce=None): - r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) - form = aiohttp.FormData() - - payload = {'tts': tts} - if content: - payload['content'] = content - if embed: - payload['embed'] = embed - if nonce: - payload['nonce'] = nonce - - form.add_field('payload_json', utils.to_json(payload)) - if len(files) == 1: - fp = files[0] - form.add_field('file', fp[0], filename=fp[1], content_type='application/octet-stream') - else: - for index, (buffer, filename) in enumerate(files): - form.add_field('file%s' % index, buffer, filename=filename, content_type='application/octet-stream') - - return self.request(r, data=form) - - @asyncio.coroutine - def ack_message(self, channel_id, message_id): - r = Route('POST', '/channels/{channel_id}/messages/{message_id}/ack', channel_id=channel_id, - message_id=message_id) - data = yield from self.request(r, json={'token': self._ack_token}) - self._ack_token = data['token'] - - def ack_guild(self, guild_id): - return self.request(Route('POST', '/guilds/{guild_id}/ack', guild_id=guild_id)) - - def delete_message(self, channel_id, message_id, *, reason=None): - r = Route('DELETE', '/channels/{channel_id}/messages/{message_id}', channel_id=channel_id, - message_id=message_id) - return self.request(r, reason=reason) - - def delete_messages(self, channel_id, message_ids, *, reason=None): - r = Route('POST', '/channels/{channel_id}/messages/bulk_delete', channel_id=channel_id) - payload = { - 'messages': message_ids - } - - return self.request(r, json=payload, reason=reason) - - def edit_message(self, message_id, channel_id, **fields): - r = Route('PATCH', '/channels/{channel_id}/messages/{message_id}', channel_id=channel_id, - message_id=message_id) - return self.request(r, json=fields) - - def add_reaction(self, message_id, channel_id, emoji): - r = Route('PUT', '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me', - channel_id=channel_id, message_id=message_id, emoji=emoji) - return self.request(r, header_bypass_delay=0.25) - - def remove_reaction(self, message_id, channel_id, emoji, member_id): - r = Route('DELETE', '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{member_id}', - channel_id=channel_id, message_id=message_id, member_id=member_id, emoji=emoji) - return self.request(r, header_bypass_delay=0.25) - - def get_reaction_users(self, message_id, channel_id, emoji, limit, after=None): - r = Route('GET', '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}', - channel_id=channel_id, message_id=message_id, emoji=emoji) - - params = {'limit': limit} - if after: - params['after'] = after - return self.request(r, params=params) - - def clear_reactions(self, message_id, channel_id): - r = Route('DELETE', '/channels/{channel_id}/messages/{message_id}/reactions', - channel_id=channel_id, message_id=message_id) - - return self.request(r) - - def get_message(self, channel_id, message_id): - r = Route('GET', '/channels/{channel_id}/messages/{message_id}', channel_id=channel_id, message_id=message_id) - return self.request(r) - - def logs_from(self, channel_id, limit, before=None, after=None, around=None): - params = { - 'limit': limit - } - - if before: - params['before'] = before - if after: - params['after'] = after - if around: - params['around'] = around - - return self.request(Route('GET', '/channels/{channel_id}/messages', channel_id=channel_id), params=params) - - def pin_message(self, channel_id, message_id): - return self.request(Route('PUT', '/channels/{channel_id}/pins/{message_id}', - channel_id=channel_id, message_id=message_id)) - - def unpin_message(self, channel_id, message_id): - return self.request(Route('DELETE', '/channels/{channel_id}/pins/{message_id}', - channel_id=channel_id, message_id=message_id)) - - def pins_from(self, channel_id): - return self.request(Route('GET', '/channels/{channel_id}/pins', channel_id=channel_id)) - - # Member management - - def kick(self, user_id, guild_id, reason=None): - r = Route('DELETE', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) - if reason: - # thanks aiohttp - r.url = '{0.url}?reason={1}'.format(r, _uriquote(reason)) - - return self.request(r) - - def ban(self, user_id, guild_id, delete_message_days=1, reason=None): - r = Route('PUT', '/guilds/{guild_id}/bans/{user_id}', guild_id=guild_id, user_id=user_id) - params = { - 'delete-message-days': delete_message_days, - } - - if reason: - # thanks aiohttp - r.url = '{0.url}?reason={1}'.format(r, _uriquote(reason)) - - return self.request(r, params=params) - - def unban(self, user_id, guild_id, *, reason=None): - r = Route('DELETE', '/guilds/{guild_id}/bans/{user_id}', guild_id=guild_id, user_id=user_id) - return self.request(r, reason=reason) - - def guild_voice_state(self, user_id, guild_id, *, mute=None, deafen=None, reason=None): - r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) - payload = {} - if mute is not None: - payload['mute'] = mute - - if deafen is not None: - payload['deaf'] = deafen - - return self.request(r, json=payload, reason=reason) - - def edit_profile(self, password, username, avatar, **fields): - payload = { - 'password': password, - 'username': username, - 'avatar': avatar - } - - if 'email' in fields: - payload['email'] = fields['email'] - - if 'new_password' in fields: - payload['new_password'] = fields['new_password'] - - return self.request(Route('PATCH', '/users/@me'), json=payload) - - def change_my_nickname(self, guild_id, nickname, *, reason=None): - r = Route('PATCH', '/guilds/{guild_id}/members/@me/nick', guild_id=guild_id) - payload = { - 'nick': nickname - } - return self.request(r, json=payload, reason=reason) - - def change_nickname(self, guild_id, user_id, nickname, *, reason=None): - r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) - payload = { - 'nick': nickname - } - return self.request(r, json=payload, reason=reason) - - def edit_member(self, guild_id, user_id, *, reason=None, **fields): - r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) - return self.request(r, json=fields, reason=reason) - - # Channel management - - def edit_channel(self, channel_id, *, reason=None, **options): - r = Route('PATCH', '/channels/{channel_id}', channel_id=channel_id) - valid_keys = ('name', 'topic', 'bitrate', 'nsfw', 'user_limit', 'position', 'permission_overwrites') - payload = { - k: v for k, v in options.items() if k in valid_keys - } - - return self.request(r, reason=reason, json=payload) - - def bulk_channel_update(self, guild_id, data, *, reason=None): - r = Route('PATCH', '/guilds/{guild_id}/channels', guild_id=guild_id) - return self.request(r, json=data, reason=reason) - - def create_channel(self, guild_id, name, channel_type, permission_overwrites=None, *, reason=None): - payload = { - 'name': name, - 'type': channel_type - } - - if permission_overwrites is not None: - payload['permission_overwrites'] = permission_overwrites - - return self.request(Route('POST', '/guilds/{guild_id}/channels', guild_id=guild_id), json=payload, reason=reason) - - def delete_channel(self, channel_id, *, reason=None): - return self.request(Route('DELETE', '/channels/{channel_id}', channel_id=channel_id), reason=reason) - - # Webhook management - - def create_webhook(self, channel_id, *, name=None, avatar=None): - payload = {} - if name is not None: - payload['name'] = name - if avatar is not None: - payload['avatar'] = avatar - - return self.request(Route('POST', '/channels/{channel_id}/webhooks', channel_id=channel_id), json=payload) - - def channel_webhooks(self, channel_id): - return self.request(Route('GET', '/channels/{channel_id}/webhooks', channel_id=channel_id)) - - def guild_webhooks(self, guild_id): - return self.request(Route('GET', '/guilds/{guild_id}/webhooks', guild_id=guild_id)) - - def get_webhook(self, webhook_id): - return self.request(Route('GET', '/webhooks/{webhook_id}', webhook_id=webhook_id)) - - # Guild management - - def leave_guild(self, guild_id): - return self.request(Route('DELETE', '/users/@me/guilds/{guild_id}', guild_id=guild_id)) - - def delete_guild(self, guild_id): - return self.request(Route('DELETE', '/guilds/{guild_id}', guild_id=guild_id)) - - def create_guild(self, name, region, icon): - payload = { - 'name': name, - 'icon': icon, - 'region': region - } - - return self.request(Route('POST', '/guilds'), json=payload) - - def edit_guild(self, guild_id, *, reason=None, **fields): - valid_keys = ('name', 'region', 'icon', 'afk_timeout', 'owner_id', - 'afk_channel_id', 'splash', 'verification_level', - 'system_channel_id') - - payload = { - k: v for k, v in fields.items() if k in valid_keys - } - - return self.request(Route('PATCH', '/guilds/{guild_id}', guild_id=guild_id), json=payload, reason=reason) - - def get_bans(self, guild_id): - return self.request(Route('GET', '/guilds/{guild_id}/bans', guild_id=guild_id)) - - def get_vanity_code(self, guild_id): - return self.request(Route('GET', '/guilds/{guild_id}/vanity-url', guild_id=guild_id)) - - def change_vanity_code(self, guild_id, code, *, reason=None): - payload = { 'code': code } - return self.request(Route('PATCH', '/guilds/{guild_id}/vanity-url', guild_id=guild_id), json=payload, reason=reason) - - def prune_members(self, guild_id, days, *, reason=None): - params = { - 'days': days - } - return self.request(Route('POST', '/guilds/{guild_id}/prune', guild_id=guild_id), params=params, reason=reason) - - def estimate_pruned_members(self, guild_id, days): - params = { - 'days': days - } - return self.request(Route('GET', '/guilds/{guild_id}/prune', guild_id=guild_id), params=params) - - def create_custom_emoji(self, guild_id, name, image, *, reason=None): - payload = { - 'name': name, - 'image': image - } - - r = Route('POST', '/guilds/{guild_id}/emojis', guild_id=guild_id) - return self.request(r, json=payload, reason=reason) - - def delete_custom_emoji(self, guild_id, emoji_id, *, reason=None): - r = Route('DELETE', '/guilds/{guild_id}/emojis/{emoji_id}', guild_id=guild_id, emoji_id=emoji_id) - return self.request(r, reason=reason) - - def edit_custom_emoji(self, guild_id, emoji_id, *, name, reason=None): - payload = { - 'name': name - } - r = Route('PATCH', '/guilds/{guild_id}/emojis/{emoji_id}', guild_id=guild_id, emoji_id=emoji_id) - return self.request(r, json=payload, reason=reason) - - def get_audit_logs(self, guild_id, limit=100, before=None, after=None, user_id=None, action_type=None): - params = { 'limit': limit } - if before: - params['before'] = before - if after: - params['after'] = after - if user_id: - params['user_id'] = user_id - if action_type: - params['action_type'] = action_type - - r = Route('GET', '/guilds/{guild_id}/audit-logs', guild_id=guild_id) - return self.request(r, params=params) - - # Invite management - - def create_invite(self, channel_id, *, reason=None, **options): - r = Route('POST', '/channels/{channel_id}/invites', channel_id=channel_id) - payload = { - 'max_age': options.get('max_age', 0), - 'max_uses': options.get('max_uses', 0), - 'temporary': options.get('temporary', False), - 'unique': options.get('unique', True) - } - - return self.request(r, reason=reason, json=payload) - - def get_invite(self, invite_id): - return self.request(Route('GET', '/invite/{invite_id}', invite_id=invite_id)) - - def invites_from(self, guild_id): - return self.request(Route('GET', '/guilds/{guild_id}/invites', guild_id=guild_id)) - - def invites_from_channel(self, channel_id): - return self.request(Route('GET', '/channels/{channel_id}/invites', channel_id=channel_id)) - - def delete_invite(self, invite_id, *, reason=None): - return self.request(Route('DELETE', '/invite/{invite_id}', invite_id=invite_id), reason=reason) - - # Role management - - def edit_role(self, guild_id, role_id, *, reason=None, **fields): - r = Route('PATCH', '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id) - valid_keys = ('name', 'permissions', 'color', 'hoist', 'mentionable') - payload = { - k: v for k, v in fields.items() if k in valid_keys - } - return self.request(r, json=payload, reason=reason) - - def delete_role(self, guild_id, role_id, *, reason=None): - r = Route('DELETE', '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id) - return self.request(r, reason=reason) - - def replace_roles(self, user_id, guild_id, role_ids, *, reason=None): - return self.edit_member(guild_id=guild_id, user_id=user_id, roles=role_ids, reason=reason) - - def create_role(self, guild_id, *, reason=None, **fields): - r = Route('POST', '/guilds/{guild_id}/roles', guild_id=guild_id) - return self.request(r, json=fields, reason=reason) - - def move_role_position(self, guild_id, positions, *, reason=None): - r = Route('PATCH', '/guilds/{guild_id}/roles', guild_id=guild_id) - return self.request(r, json=positions, reason=reason) - - def add_role(self, guild_id, user_id, role_id, *, reason=None): - r = Route('PUT', '/guilds/{guild_id}/members/{user_id}/roles/{role_id}', - guild_id=guild_id, user_id=user_id, role_id=role_id) - return self.request(r, reason=reason) - - def remove_role(self, guild_id, user_id, role_id, *, reason=None): - r = Route('DELETE', '/guilds/{guild_id}/members/{user_id}/roles/{role_id}', - guild_id=guild_id, user_id=user_id, role_id=role_id) - return self.request(r, reason=reason) - - def edit_channel_permissions(self, channel_id, target, allow, deny, type, *, reason=None): - payload = { - 'id': target, - 'allow': allow, - 'deny': deny, - 'type': type - } - r = Route('PUT', '/channels/{channel_id}/permissions/{target}', channel_id=channel_id, target=target) - return self.request(r, json=payload, reason=reason) - - def delete_channel_permissions(self, channel_id, target, *, reason=None): - r = Route('DELETE', '/channels/{channel_id}/permissions/{target}', channel_id=channel_id, target=target) - return self.request(r, reason=reason) - - # Voice management - - def move_member(self, user_id, guild_id, channel_id, *, reason=None): - return self.edit_member(guild_id=guild_id, user_id=user_id, channel_id=channel_id, reason=reason) - - # Relationship related - - def remove_relationship(self, user_id): - r = Route('DELETE', '/users/@me/relationships/{user_id}', user_id=user_id) - return self.request(r) - - def add_relationship(self, user_id, type=None): - r = Route('PUT', '/users/@me/relationships/{user_id}', user_id=user_id) - payload = {} - if type is not None: - payload['type'] = type - - return self.request(r, json=payload) - - def send_friend_request(self, username, discriminator): - r = Route('POST', '/users/@me/relationships') - payload = { - 'username': username, - 'discriminator': int(discriminator) - } - return self.request(r, json=payload) - - # Misc - - def application_info(self): - return self.request(Route('GET', '/oauth2/applications/@me')) - - @asyncio.coroutine - def get_gateway(self): - try: - data = yield from self.request(Route('GET', '/gateway')) - except HTTPException as e: - raise GatewayNotFound() from e - return data.get('url') + '?encoding=json&v=6' - - @asyncio.coroutine - def get_bot_gateway(self): - try: - data = yield from self.request(Route('GET', '/gateway/bot')) - except HTTPException as e: - raise GatewayNotFound() from e - else: - return data['shards'], data['url'] + '?encoding=json&v=6' - - def get_user_info(self, user_id): - return self.request(Route('GET', '/users/{user_id}', user_id=user_id)) - - def get_user_profile(self, user_id): - return self.request(Route('GET', '/users/{user_id}/profile', user_id=user_id)) diff --git a/discord.py-rewrite/discord/invite.py b/discord.py-rewrite/discord/invite.py deleted file mode 100644 index 2af3a66..0000000 --- a/discord.py-rewrite/discord/invite.py +++ /dev/null @@ -1,158 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import asyncio - -from .utils import parse_time -from .mixins import Hashable -from .object import Object - -class Invite(Hashable): - """Represents a Discord :class:`Guild` or :class:`abc.GuildChannel` invite. - - Depending on the way this object was created, some of the attributes can - have a value of ``None``. - - .. container:: operations - - .. describe:: x == y - - Checks if two invites are equal. - - .. describe:: x != y - - Checks if two invites are not equal. - - .. describe:: hash(x) - - Returns the invite hash. - - .. describe:: str(x) - - Returns the invite URL. - - Attributes - ----------- - max_age: int - How long the before the invite expires in seconds. A value of 0 indicates that it doesn't expire. - code: str - The URL fragment used for the invite. - guild: :class:`Guild` - The guild the invite is for. - revoked: bool - Indicates if the invite has been revoked. - created_at: `datetime.datetime` - A datetime object denoting the time the invite was created. - temporary: bool - Indicates that the invite grants temporary membership. - If True, members who joined via this invite will be kicked upon disconnect. - uses: int - How many times the invite has been used. - max_uses: int - How many times the invite can be used. - inviter: :class:`User` - The user who created the invite. - channel: :class:`abc.GuildChannel` - The channel the invite is for. - """ - - - __slots__ = ( 'max_age', 'code', 'guild', 'revoked', 'created_at', 'uses', - 'temporary', 'max_uses', 'inviter', 'channel', '_state' ) - - def __init__(self, *, state, data): - self._state = state - self.max_age = data.get('max_age') - self.code = data.get('code') - self.guild = data.get('guild') - self.revoked = data.get('revoked') - self.created_at = parse_time(data.get('created_at')) - self.temporary = data.get('temporary') - self.uses = data.get('uses') - self.max_uses = data.get('max_uses') - - inviter_data = data.get('inviter') - self.inviter = None if inviter_data is None else self._state.store_user(inviter_data) - self.channel = data.get('channel') - - @classmethod - def from_incomplete(cls, *, state, data): - guild_id = int(data['guild']['id']) - channel_id = int(data['channel']['id']) - guild = state._get_guild(guild_id) - if guild is not None: - channel = guild.get_channel(channel_id) - else: - guild = Object(id=guild_id) - channel = Object(id=channel_id) - guild.name = data['guild']['name'] - channel.name = data['channel']['name'] - - data['guild'] = guild - data['channel'] = channel - return cls(state=state, data=data) - - def __str__(self): - return self.url - - def __repr__(self): - return ''.format(self) - - def __hash__(self): - return hash(self.code) - - @property - def id(self): - """Returns the proper code portion of the invite.""" - return self.code - - @property - def url(self): - """A property that retrieves the invite URL.""" - return 'http://discord.gg/' + self.code - - @asyncio.coroutine - def delete(self, *, reason=None): - """|coro| - - Revokes the instant invite. - - Parameters - ----------- - reason: Optional[str] - The reason for deleting this invite. Shows up on the audit log. - - Raises - ------- - Forbidden - You do not have permissions to revoke invites. - NotFound - The invite is invalid or expired. - HTTPException - Revoking the invite failed. - """ - - yield from self._state.http.delete_invite(self.code, reason=reason) diff --git a/discord.py-rewrite/discord/iterators.py b/discord.py-rewrite/discord/iterators.py deleted file mode 100644 index e6f5234..0000000 --- a/discord.py-rewrite/discord/iterators.py +++ /dev/null @@ -1,484 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import sys -import asyncio -import datetime - -from .errors import NoMoreItems -from .utils import time_snowflake, maybe_coroutine -from .object import Object -from .audit_logs import AuditLogEntry - -PY35 = sys.version_info >= (3, 5) - -class _AsyncIterator: - __slots__ = () - - def get(self, **attrs): - def predicate(elem): - for attr, val in attrs.items(): - nested = attr.split('__') - obj = elem - for attribute in nested: - obj = getattr(obj, attribute) - - if obj != val: - return False - return True - - return self.find(predicate) - - @asyncio.coroutine - def find(self, predicate): - while True: - try: - elem = yield from self.next() - except NoMoreItems: - return None - - ret = yield from maybe_coroutine(predicate, elem) - if ret: - return elem - - def map(self, func): - return _MappedAsyncIterator(self, func) - - def filter(self, predicate): - return _FilteredAsyncIterator(self, predicate) - - @asyncio.coroutine - def flatten(self): - ret = [] - while True: - try: - item = yield from self.next() - except NoMoreItems: - return ret - else: - ret.append(item) - - if PY35: - @asyncio.coroutine - def __aiter__(self): - return self - - @asyncio.coroutine - def __anext__(self): - try: - msg = yield from self.next() - except NoMoreItems: - raise StopAsyncIteration() - else: - return msg - -def _identity(x): - return x - -class _MappedAsyncIterator(_AsyncIterator): - def __init__(self, iterator, func): - self.iterator = iterator - self.func = func - - @asyncio.coroutine - def next(self): - # this raises NoMoreItems and will propagate appropriately - item = yield from self.iterator.next() - return (yield from maybe_coroutine(self.func, item)) - -class _FilteredAsyncIterator(_AsyncIterator): - def __init__(self, iterator, predicate): - self.iterator = iterator - - if predicate is None: - predicate = _identity - - self.predicate = predicate - - @asyncio.coroutine - def next(self): - getter = self.iterator.next - pred = self.predicate - while True: - # propagate NoMoreItems similar to _MappedAsyncIterator - item = yield from getter() - ret = yield from maybe_coroutine(pred, item) - if ret: - return item - -class ReactionIterator(_AsyncIterator): - def __init__(self, message, emoji, limit=100, after=None): - self.message = message - self.limit = limit - self.after = after - state = message._state - self.getter = state.http.get_reaction_users - self.state = state - self.emoji = emoji - self.guild = message.guild - self.channel_id = message.channel.id - self.users = asyncio.Queue(loop=state.loop) - - @asyncio.coroutine - def next(self): - if self.users.empty(): - yield from self.fill_users() - - try: - return self.users.get_nowait() - except asyncio.QueueEmpty: - raise NoMoreItems() - - @asyncio.coroutine - def fill_users(self): - # this is a hack because >circular imports< - from .user import User - - if self.limit > 0: - retrieve = self.limit if self.limit <= 100 else 100 - - after = self.after.id if self.after else None - data = yield from self.getter(self.message.id, self.channel_id, self.emoji, retrieve, after=after) - - if data: - self.limit -= retrieve - self.after = Object(id=int(data[0]['id'])) - - if self.guild is None: - for element in reversed(data): - yield from self.users.put(User(state=self.state, data=element)) - else: - for element in reversed(data): - member_id = int(element['id']) - member = self.guild.get_member(member_id) - if member is not None: - yield from self.users.put(member) - else: - yield from self.users.put(User(state=self.state, data=element)) - -class HistoryIterator(_AsyncIterator): - """Iterator for receiving a channel's message history. - - The messages endpoint has two behaviours we care about here: - If `before` is specified, the messages endpoint returns the `limit` - newest messages before `before`, sorted with newest first. For filling over - 100 messages, update the `before` parameter to the oldest message received. - Messages will be returned in order by time. - If `after` is specified, it returns the `limit` oldest messages after - `after`, sorted with newest first. For filling over 100 messages, update the - `after` parameter to the newest message received. If messages are not - reversed, they will be out of order (99-0, 199-100, so on) - - A note that if both before and after are specified, before is ignored by the - messages endpoint. - - Parameters - ----------- - messageable: :class:`abc.Messageable` - Messageable class to retrieve message history fro. - limit : int - Maximum number of messages to retrieve - before : :class:`Message` or id-like - Message before which all messages must be. - after : :class:`Message` or id-like - Message after which all messages must be. - around : :class:`Message` or id-like - Message around which all messages must be. Limit max 101. Note that if - limit is an even number, this will return at most limit+1 messages. - reverse: bool - If set to true, return messages in oldest->newest order. Recommended - when using with "after" queries with limit over 100, otherwise messages - will be out of order. - """ - - def __init__(self, messageable, limit, - before=None, after=None, around=None, reverse=None): - - if isinstance(before, datetime.datetime): - before = Object(id=time_snowflake(before, high=False)) - if isinstance(after, datetime.datetime): - after = Object(id=time_snowflake(after, high=True)) - if isinstance(around, datetime.datetime): - around = Object(id=time_snowflake(around)) - - self.messageable = messageable - self.limit = limit - self.before = before - self.after = after - self.around = around - - if reverse is None: - self.reverse = after is not None - else: - self.reverse = reverse - - self._filter = None # message dict -> bool - - self.state = self.messageable._state - self.logs_from = self.state.http.logs_from - self.messages = asyncio.Queue(loop=self.state.loop) - - if self.around: - if self.limit is None: - raise ValueError('history does not support around with limit=None') - if self.limit > 101: - raise ValueError("history max limit 101 when specifying around parameter") - elif self.limit == 101: - self.limit = 100 # Thanks discord - elif self.limit == 1: - raise ValueError("Use get_message.") - - self._retrieve_messages = self._retrieve_messages_around_strategy - if self.before and self.after: - self._filter = lambda m: self.after.id < int(m['id']) < self.before.id - elif self.before: - self._filter = lambda m: int(m['id']) < self.before.id - elif self.after: - self._filter = lambda m: self.after.id < int(m['id']) - elif self.before and self.after: - if self.reverse: - self._retrieve_messages = self._retrieve_messages_after_strategy - self._filter = lambda m: int(m['id']) < self.before.id - else: - self._retrieve_messages = self._retrieve_messages_before_strategy - self._filter = lambda m: int(m['id']) > self.after.id - elif self.after: - self._retrieve_messages = self._retrieve_messages_after_strategy - else: - self._retrieve_messages = self._retrieve_messages_before_strategy - - @asyncio.coroutine - def next(self): - if self.messages.empty(): - yield from self.fill_messages() - - try: - return self.messages.get_nowait() - except asyncio.QueueEmpty: - raise NoMoreItems() - - def _get_retrieve(self): - l = self.limit - if l is None: - r = 100 - elif l <= 100: - r = l - else: - r = 100 - - self.retrieve = r - return r > 0 - - @asyncio.coroutine - def flatten(self): - # this is similar to fill_messages except it uses a list instead - # of a queue to place the messages in. - result = [] - channel = yield from self.messageable._get_channel() - self.channel = channel - while self._get_retrieve(): - data = yield from self._retrieve_messages(self.retrieve) - if len(data) < 100: - self.limit = 0 # terminate the infinite loop - - if self.reverse: - data = reversed(data) - if self._filter: - data = filter(self._filter, data) - - for element in data: - result.append(self.state.create_message(channel=channel, data=element)) - return result - - @asyncio.coroutine - def fill_messages(self): - if not hasattr(self, 'channel'): - # do the required set up - channel = yield from self.messageable._get_channel() - self.channel = channel - - if self._get_retrieve(): - data = yield from self._retrieve_messages(self.retrieve) - if self.limit is None and len(data) < 100: - self.limit = 0 # terminate the infinite loop - - if self.reverse: - data = reversed(data) - if self._filter: - data = filter(self._filter, data) - - channel = self.channel - for element in data: - yield from self.messages.put(self.state.create_message(channel=channel, data=element)) - - @asyncio.coroutine - def _retrieve_messages(self, retrieve): - """Retrieve messages and update next parameters.""" - pass - - @asyncio.coroutine - def _retrieve_messages_before_strategy(self, retrieve): - """Retrieve messages using before parameter.""" - before = self.before.id if self.before else None - data = yield from self.logs_from(self.channel.id, retrieve, before=before) - if len(data): - if self.limit is not None: - self.limit -= retrieve - self.before = Object(id=int(data[-1]['id'])) - return data - - @asyncio.coroutine - def _retrieve_messages_after_strategy(self, retrieve): - """Retrieve messages using after parameter.""" - after = self.after.id if self.after else None - data = yield from self.logs_from(self.channel.id, retrieve, after=after) - if len(data): - if self.limit is not None: - self.limit -= retrieve - self.after = Object(id=int(data[0]['id'])) - return data - - @asyncio.coroutine - def _retrieve_messages_around_strategy(self, retrieve): - """Retrieve messages using around parameter.""" - if self.around: - around = self.around.id if self.around else None - data = yield from self.logs_from(self.channel.id, retrieve, around=around) - self.around = None - return data - return [] - -class AuditLogIterator(_AsyncIterator): - def __init__(self, guild, limit=None, before=None, after=None, reverse=None, user_id=None, action_type=None): - if isinstance(before, datetime.datetime): - before = Object(id=time_snowflake(before, high=False)) - if isinstance(after, datetime.datetime): - after = Object(id=time_snowflake(after, high=True)) - - - self.guild = guild - self.loop = guild._state.loop - self.request = guild._state.http.get_audit_logs - self.limit = limit - self.before = before - self.user_id = user_id - self.action_type = action_type - self.after = after - self._users = {} - self._state = guild._state - - if reverse is None: - self.reverse = after is not None - else: - self.reverse = reverse - - self._filter = None # entry dict -> bool - - self.entries = asyncio.Queue(loop=self.loop) - - if self.before and self.after: - if self.reverse: - self._strategy = self._after_strategy - self._filter = lambda m: int(m['id']) < self.before.id - else: - self._strategy = self._before_strategy - self._filter = lambda m: int(m['id']) > self.after.id - elif self.after: - self._strategy = self._after_strategy - else: - self._strategy = self._before_strategy - - @asyncio.coroutine - def _before_strategy(self, retrieve): - before = self.before.id if self.before else None - data = yield from self.request(self.guild.id, limit=retrieve, user_id=self.user_id, - action_type=self.action_type, before=before) - - entries = data.get('audit_log_entries', []) - if len(data) and entries: - if self.limit is not None: - self.limit -= retrieve - self.before = Object(id=int(entries[-1]['id'])) - return data.get('users', []), entries - - @asyncio.coroutine - def _after_strategy(self, retrieve): - after = self.after.id if self.after else None - data = yield from self.request(self.guild.id, limit=retrieve, user_id=self.user_id, - action_type=self.action_type, after=after) - entries = data.get('audit_log_entries', []) - if len(data) and entries: - if self.limit is not None: - self.limit -= retrieve - self.after = Object(id=int(entries[0]['id'])) - return data.get('users', []), entries - - @asyncio.coroutine - def next(self): - if self.entries.empty(): - yield from self._fill() - - try: - return self.entries.get_nowait() - except asyncio.QueueEmpty: - raise NoMoreItems() - - def _get_retrieve(self): - l = self.limit - if l is None: - r = 100 - elif l <= 100: - r = l - else: - r = 100 - - self.retrieve = r - return r > 0 - - @asyncio.coroutine - def _fill(self): - from .user import User - - if self._get_retrieve(): - users, data = yield from self._strategy(self.retrieve) - if self.limit is None and len(data) < 100: - self.limit = 0 # terminate the infinite loop - - if self.reverse: - data = reversed(data) - if self._filter: - data = filter(self._filter, data) - - for user in users: - u = User(data=user, state=self._state) - self._users[u.id] = u - - for element in data: - # TODO: remove this if statement later - if element['action_type'] is None: - continue - - yield from self.entries.put(AuditLogEntry(data=element, users=self._users, guild=self.guild)) diff --git a/discord.py-rewrite/discord/member.py b/discord.py-rewrite/discord/member.py deleted file mode 100644 index 711bc76..0000000 --- a/discord.py-rewrite/discord/member.py +++ /dev/null @@ -1,559 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import asyncio -import itertools -import copy - -import discord.abc - -from . import utils -from .user import BaseUser, User -from .game import Game -from .permissions import Permissions -from .enums import Status, try_enum -from .colour import Colour -from .object import Object - -class VoiceState: - """Represents a Discord user's voice state. - - Attributes - ------------ - deaf: bool - Indicates if the user is currently deafened by the guild. - mute: bool - Indicates if the user is currently muted by the guild. - self_mute: bool - Indicates if the user is currently muted by their own accord. - self_deaf: bool - Indicates if the user is currently deafened by their own accord. - afk: bool - Indicates if the user is currently in the AFK channel in the guild. - channel: :class:`VoiceChannel` - The voice channel that the user is currently connected to. None if the user - is not currently in a voice channel. - """ - - __slots__ = ( 'session_id', 'deaf', 'mute', 'self_mute', - 'self_deaf', 'afk', 'channel' ) - - def __init__(self, *, data, channel=None): - self.session_id = data.get('session_id') - self._update(data, channel) - - def _update(self, data, channel): - self.self_mute = data.get('self_mute', False) - self.self_deaf = data.get('self_deaf', False) - self.afk = data.get('suppress', False) - self.mute = data.get('mute', False) - self.deaf = data.get('deaf', False) - self.channel = channel - - def __repr__(self): - return ''.format(self) - -def flatten_user(cls): - for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()): - # ignore private/special methods - if attr.startswith('_'): - continue - - # don't override what we already have - if attr in cls.__dict__: - continue - - # if it's a slotted attribute or a property, redirect it - # slotted members are implemented as member_descriptors in Type.__dict__ - if not hasattr(value, '__annotations__'): - def getter(self, x=attr): - return getattr(self._user, x) - setattr(cls, attr, property(getter, doc='Equivalent to :attr:`User.%s`' % attr)) - else: - # probably a member function by now - def generate_function(x): - def general(self, *args, **kwargs): - return getattr(self._user, x)(*args, **kwargs) - - general.__name__ = x - return general - - func = generate_function(attr) - func.__doc__ = value.__doc__ - setattr(cls, attr, func) - - return cls - -_BaseUser = discord.abc.User - -@flatten_user -class Member(discord.abc.Messageable, _BaseUser): - """Represents a Discord member to a :class:`Guild`. - - This implements a lot of the functionality of :class:`User`. - - .. container:: operations - - .. describe:: x == y - - Checks if two members are equal. - Note that this works with :class:`User` instances too. - - .. describe:: x != y - - Checks if two members are not equal. - Note that this works with :class:`User` instances too. - - .. describe:: hash(x) - - Returns the member's hash. - - .. describe:: str(x) - - Returns the member's name with the discriminator. - - Attributes - ---------- - roles - A list of :class:`Role` that the member belongs to. Note that the first element of this - list is always the default '@everyone' role. These roles are sorted by their position - in the role hierarchy. - joined_at : `datetime.datetime` - A datetime object that specifies the date and time in UTC that the member joined the guild for - the first time. - status : :class:`Status` - The member's status. There is a chance that the status will be a ``str`` - if it is a value that is not recognised by the enumerator. - game : :class:`Game` - The game that the user is currently playing. Could be None if no game is being played. - guild : :class:`Guild` - The guild that the member belongs to. - nick : Optional[str] - The guild specific nickname of the user. - """ - - __slots__ = ('roles', 'joined_at', 'status', 'game', 'guild', 'nick', '_user', '_state') - - def __init__(self, *, data, guild, state): - self._state = state - self._user = state.store_user(data['user']) - self.guild = guild - self.joined_at = utils.parse_time(data.get('joined_at')) - self._update_roles(data) - self.status = Status.offline - game = data.get('game', {}) - self.game = Game(**game) if game else None - self.nick = data.get('nick', None) - - def __str__(self): - return str(self._user) - - def __repr__(self): - return ''.format(self, self._user) - - def __eq__(self, other): - return isinstance(other, _BaseUser) and other.id == self.id - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return hash(self._user.id) - - @asyncio.coroutine - def _get_channel(self): - ch = yield from self.create_dm() - return ch - - def _update_roles(self, data): - # update the roles - self.roles = [self.guild.default_role] - for roleid in map(int, data['roles']): - role = utils.find(lambda r: r.id == roleid, self.guild.roles) - if role is not None: - self.roles.append(role) - - # sort the roles by hierarchy since they can be "randomised" - self.roles.sort() - - def _update(self, data, user=None): - if user: - self._user.name = user['username'] - self._user.discriminator = user['discriminator'] - self._user.avatar = user['avatar'] - self._user.bot = user.get('bot', False) - - # the nickname change is optional, - # if it isn't in the payload then it didn't change - try: - self.nick = data['nick'] - except KeyError: - pass - - self._update_roles(data) - - def _presence_update(self, data, user): - self.status = try_enum(Status, data['status']) - game = data.get('game', {}) - self.game = Game(**game) if game else None - u = self._user - u.name = user.get('username', u.name) - u.avatar = user.get('avatar', u.avatar) - u.discriminator = user.get('discriminator', u.discriminator) - - def _copy(self): - c = copy.copy(self) - c._user = copy.copy(self._user) - return c - - @property - def colour(self): - """A property that returns a :class:`Colour` denoting the rendered colour - for the member. If the default colour is the one rendered then an instance - of :meth:`Colour.default` is returned. - - There is an alias for this under ``color``. - """ - - roles = self.roles[1:] # remove @everyone - - # highest order of the colour is the one that gets rendered. - # if the highest is the default colour then the next one with a colour - # is chosen instead - for role in reversed(roles): - if role.colour.value: - return role.colour - return Colour.default() - - color = colour - - @property - def mention(self): - """Returns a string that mentions the member.""" - if self.nick: - return '<@!%s>' % self.id - return '<@%s>' % self.id - - @property - def display_name(self): - """Returns the user's display name. - - For regular users this is just their username, but - if they have a guild specific nickname then that - is returned instead. - """ - return self.nick if self.nick is not None else self.name - - def mentioned_in(self, message): - """Checks if the member is mentioned in the specified message. - - Parameters - ----------- - message: :class:`Message` - The message to check if you're mentioned in. - """ - if self._user.mentioned_in(message): - return True - - for role in message.role_mentions: - has_role = utils.get(self.roles, id=role.id) is not None - if has_role: - return True - - return False - - def permissions_in(self, channel): - """An alias for :meth:`abc.GuildChannel.permissions_for`. - - Basically equivalent to: - - .. code-block:: python3 - - channel.permissions_for(self) - - Parameters - ----------- - channel - The channel to check your permissions for. - """ - return channel.permissions_for(self) - - @property - def top_role(self): - """Returns the member's highest role. - - This is useful for figuring where a member stands in the role - hierarchy chain. - """ - return self.roles[-1] - - @property - def guild_permissions(self): - """Returns the member's guild permissions. - - This only takes into consideration the guild permissions - and not most of the implied permissions or any of the - channel permission overwrites. For 100% accurate permission - calculation, please use either :meth:`permissions_in` or - :meth:`abc.GuildChannel.permissions_for`. - - This does take into consideration guild ownership and the - administrator implication. - """ - - if self.guild.owner == self: - return Permissions.all() - - base = Permissions.none() - for r in self.roles: - base.value |= r.permissions.value - - if base.administrator: - return Permissions.all() - - return base - - @property - def voice(self): - """Optional[:class:`VoiceState`]: Returns the member's current voice state.""" - return self.guild._voice_state_for(self._user.id) - - @asyncio.coroutine - def ban(self, **kwargs): - """|coro| - - Bans this member. Equivalent to :meth:`Guild.ban` - """ - yield from self.guild.ban(self, **kwargs) - - @asyncio.coroutine - def unban(self, *, reason=None): - """|coro| - - Unbans this member. Equivalent to :meth:`Guild.unban` - """ - yield from self.guild.unban(self, reason=reason) - - @asyncio.coroutine - def kick(self, *, reason=None): - """|coro| - - Kicks this member. Equivalent to :meth:`Guild.kick` - """ - yield from self.guild.kick(self, reason=reason) - - @asyncio.coroutine - def edit(self, *, reason=None, **fields): - """|coro| - - Edits the member's data. - - Depending on the parameter passed, this requires different permissions listed below: - - +---------------+--------------------------------------+ - | Parameter | Permission | - +---------------+--------------------------------------+ - | nick | :attr:`Permissions.manage_nicknames` | - +---------------+--------------------------------------+ - | mute | :attr:`Permissions.mute_members` | - +---------------+--------------------------------------+ - | deafen | :attr:`Permissions.deafen_members` | - +---------------+--------------------------------------+ - | roles | :attr:`Permissions.manage_roles` | - +---------------+--------------------------------------+ - | voice_channel | :attr:`Permissions.move_members` | - +---------------+--------------------------------------+ - - All parameters are optional. - - Parameters - ----------- - nick: str - The member's new nickname. Use ``None`` to remove the nickname. - mute: bool - Indicates if the member should be guild muted or un-muted. - deafen: bool - Indicates if the member should be guild deafened or un-deafened. - roles: List[:class:`Roles`] - The member's new list of roles. This *replaces* the roles. - voice_channel: :class:`VoiceChannel` - The voice channel to move the member to. - reason: Optional[str] - The reason for editing this member. Shows up on the audit log. - - Raises - ------- - Forbidden - You do not have the proper permissions to the action requested. - HTTPException - The operation failed. - """ - http = self._state.http - guild_id = self.guild.id - payload = {} - - try: - nick = fields['nick'] - except KeyError: - # nick not present so... - pass - else: - nick = nick if nick else '' - if self._state.self_id == self.id: - yield from http.change_my_nickname(guild_id, nick, reason=reason) - else: - payload['nick'] = nick - - deafen = fields.get('deafen') - if deafen is not None: - payload['deaf'] = deafen - - mute = fields.get('mute') - if mute is not None: - payload['mute'] = mute - - try: - vc = fields['voice_channel'] - except KeyError: - pass - else: - payload['channel_id'] = vc.id - - try: - roles = fields['roles'] - except KeyError: - pass - else: - payload['roles'] = tuple(r.id for r in roles) - - yield from http.edit_member(guild_id, self.id, reason=reason, **payload) - - # TODO: wait for WS event for modify-in-place behaviour - - @asyncio.coroutine - def move_to(self, channel, *, reason=None): - """|coro| - - Moves a member to a new voice channel (they must be connected first). - - You must have the :attr:`~Permissions.move_members` permission to - use this. - - This raises the same exceptions as :meth:`edit`. - - Parameters - ----------- - channel: :class:`VoiceChannel` - The new voice channel to move the member to. - reason: Optional[str] - The reason for doing this action. Shows up on the audit log. - """ - yield from self.edit(voice_channel=channel, reason=reason) - - @asyncio.coroutine - def add_roles(self, *roles, reason=None, atomic=True): - """|coro| - - Gives the member a number of :class:`Role`\s. - - You must have the :attr:`~Permissions.manage_roles` permission to - use this. - - Parameters - ----------- - \*roles - An argument list of :class:`abc.Snowflake` representing a :class:`Role` - to give to the member. - reason: Optional[str] - The reason for adding these roles. Shows up on the audit log. - atomic: bool - Whether to atomically add roles. This will ensure that multiple - operations will always be applied regardless of the current - state of the cache. - - Raises - ------- - Forbidden - You do not have permissions to add these roles. - HTTPException - Adding roles failed. - """ - - if not atomic: - new_roles = utils._unique(Object(id=r.id) for s in (self.roles[1:], roles) for r in s) - yield from self.edit(roles=new_roles, reason=reason) - else: - req = self._state.http.add_role - guild_id = self.guild.id - user_id = self.id - for role in roles: - yield from req(guild_id, user_id, role.id, reason=reason) - - @asyncio.coroutine - def remove_roles(self, *roles, reason=None, atomic=True): - """|coro| - - Removes :class:`Role`\s from this member. - - You must have the :attr:`~Permissions.manage_roles` permission to - use this. - - Parameters - ----------- - \*roles - An argument list of :class:`abc.Snowflake` representing a :class:`Role` - to remove from the member. - reason: Optional[str] - The reason for removing these roles. Shows up on the audit log. - atomic: bool - Whether to atomically remove roles. This will ensure that multiple - operations will always be applied regardless of the current - state of the cache. - - Raises - ------- - Forbidden - You do not have permissions to remove these roles. - HTTPException - Removing the roles failed. - """ - - if not atomic: - new_roles = [Object(id=r.id) for r in self.roles[1:]] # remove @everyone - for role in roles: - try: - new_roles.remove(Object(id=role.id)) - except ValueError: - pass - - yield from self.edit(roles=new_roles, reason=reason) - else: - req = self._state.http.remove_role - guild_id = self.guild.id - user_id = self.id - for role in roles: - yield from req(guild_id, user_id, role.id, reason=reason) diff --git a/discord.py-rewrite/discord/message.py b/discord.py-rewrite/discord/message.py deleted file mode 100644 index 15e7cfb..0000000 --- a/discord.py-rewrite/discord/message.py +++ /dev/null @@ -1,742 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import asyncio -import re - -from . import utils, compat -from .reaction import Reaction -from .emoji import Emoji, PartialReactionEmoji -from .calls import CallMessage -from .enums import MessageType, try_enum -from .errors import InvalidArgument, ClientException, HTTPException, NotFound -from .embeds import Embed - -class Attachment: - """Represents an attachment from Discord. - - Attributes - ------------ - id: int - The attachment ID. - size: int - The attachment size in bytes. - height: Optional[int] - The attachment's height, in pixels. Only applicable to images. - width: Optional[int] - The attachment's width, in pixels. Only applicable to images. - filename: str - The attachment's filename. - url: str - The attachment URL. If the message this attachment was attached - to is deleted, then this will 404. - proxy_url: str - The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the - case of images. When the message is deleted, this URL might be valid for a few - minutes or not valid at all. - """ - - __slots__ = ('id', 'size', 'height', 'width', 'filename', 'url', 'proxy_url', '_http') - - def __init__(self, *, data, state): - self.id = int(data['id']) - self.size = data['size'] - self.height = data.get('height') - self.width = data.get('width') - self.filename = data['filename'] - self.url = data.get('url') - self.proxy_url = data.get('proxy_url') - self._http = state.http - - @asyncio.coroutine - def save(self, fp): - """|coro| - - Saves this attachment into a file-like object. - - Parameters - ----------- - fp: Union[BinaryIO, str] - The file-like object to save this attachment to or the filename - to use. If a filename is passed then a file is created with that - filename and used instead. - - Raises - -------- - HTTPException - Saving the attachment failed. - NotFound - The attachment was deleted. - - Returns - -------- - int - The number of bytes written. - """ - - data = yield from self._http.get_attachment(self.url) - if isinstance(fp, str): - with open(fp, 'wb') as f: - return f.write(data) - else: - return fp.write(data) - -class Message: - """Represents a message from Discord. - - There should be no need to create one of these manually. - - Attributes - ----------- - tts: bool - Specifies if the message was done with text-to-speech. - type: :class:`MessageType` - The type of message. In most cases this should not be checked, but it is helpful - in cases where it might be a system message for :attr:`system_content`. - author - A :class:`Member` that sent the message. If :attr:`channel` is a - private channel, then it is a :class:`User` instead. - content: str - The actual contents of the message. - nonce - The value used by the discord guild and the client to verify that the message is successfully sent. - This is typically non-important. - embeds: List[:class:`Embed`] - A list embeds the message has. - channel - The :class:`TextChannel` that the message was sent from. - Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message. - call: Optional[:class:`CallMessage`] - The call that the message refers to. This is only applicable to messages of type - :attr:`MessageType.call`. - mention_everyone: bool - Specifies if the message mentions everyone. - - .. note:: - - This does not check if the ``@everyone`` text is in the message itself. - Rather this boolean indicates if the ``@everyone`` text is in the message - **and** it did end up mentioning everyone. - - mentions: list - A list of :class:`Member` that were mentioned. If the message is in a private message - then the list will be of :class:`User` instead. For messages that are not of type - :attr:`MessageType.default`\, this array can be used to aid in system messages. - For more information, see :attr:`system_content`. - - .. warning:: - - The order of the mentions list is not in any particular order so you should - not rely on it. This is a discord limitation, not one with the library. - - channel_mentions: list - A list of :class:`abc.GuildChannel` that were mentioned. If the message is in a private message - then the list is always empty. - role_mentions: list - A list of :class:`Role` that were mentioned. If the message is in a private message - then the list is always empty. - id: int - The message ID. - webhook_id: Optional[int] - If this message was sent by a webhook, then this is the webhook ID's that sent this - message. - attachments: List[:class:`Attachment`] - A list of attachments given to a message. - pinned: bool - Specifies if the message is currently pinned. - reactions : List[:class:`Reaction`] - Reactions to a message. Reactions can be either custom emoji or standard unicode emoji. - """ - - __slots__ = ( '_edited_timestamp', 'tts', 'content', 'channel', 'webhook_id', - 'mention_everyone', 'embeds', 'id', 'mentions', 'author', - '_cs_channel_mentions', '_cs_raw_mentions', 'attachments', - '_cs_clean_content', '_cs_raw_channel_mentions', 'nonce', 'pinned', - 'role_mentions', '_cs_raw_role_mentions', 'type', 'call', - '_cs_system_content', '_cs_guild', '_state', 'reactions' ) - - def __init__(self, *, state, channel, data): - self._state = state - self.id = int(data['id']) - self.webhook_id = utils._get_as_snowflake(data, 'webhook_id') - self.reactions = [Reaction(message=self, data=d) for d in data.get('reactions', [])] - self._update(channel, data) - - def __repr__(self): - return ''.format(self) - - def _try_patch(self, data, key, transform=None): - try: - value = data[key] - except KeyError: - pass - else: - if transform is None: - setattr(self, key, value) - else: - setattr(self, key, transform(value)) - - def _add_reaction(self, data, emoji, user_id): - reaction = utils.find(lambda r: r.emoji == emoji, self.reactions) - is_me = data['me'] = user_id == self._state.self_id - - if reaction is None: - reaction = Reaction(message=self, data=data, emoji=emoji) - self.reactions.append(reaction) - else: - reaction.count += 1 - if is_me: - reaction.me = is_me - - return reaction - - def _remove_reaction(self, data, emoji, user_id): - reaction = utils.find(lambda r: r.emoji == emoji, self.reactions) - - if reaction is None: - # already removed? - raise ValueError('Emoji already removed?') - - # if reaction isn't in the list, we crash. This means discord - # sent bad data, or we stored improperly - reaction.count -= 1 - - if user_id == self._state.self_id: - reaction.me = False - if reaction.count == 0: - # this raises ValueError if something went wrong as well. - self.reactions.remove(reaction) - - return reaction - - def _update(self, channel, data): - self.channel = channel - self._edited_timestamp = utils.parse_time(data.get('edited_timestamp')) - self._try_patch(data, 'pinned') - self._try_patch(data, 'mention_everyone') - self._try_patch(data, 'tts') - self._try_patch(data, 'type', lambda x: try_enum(MessageType, x)) - self._try_patch(data, 'content') - self._try_patch(data, 'attachments', lambda x: [Attachment(data=a, state=self._state) for a in x]) - self._try_patch(data, 'embeds', lambda x: list(map(Embed.from_data, x))) - self._try_patch(data, 'nonce') - - for handler in ('author', 'mentions', 'mention_roles', 'call'): - try: - getattr(self, '_handle_%s' % handler)(data[handler]) - except KeyError: - continue - - # clear the cached properties - cached = filter(lambda attr: attr.startswith('_cs_'), self.__slots__) - for attr in cached: - try: - delattr(self, attr) - except AttributeError: - pass - - def _handle_author(self, author): - self.author = self._state.store_user(author) - if self.guild is not None: - found = self.guild.get_member(self.author.id) - if found is not None: - self.author = found - - def _handle_mentions(self, mentions): - self.mentions = [] - if self.guild is None: - self.mentions = [self._state.store_user(m) for m in mentions] - return - - for mention in mentions: - id_search = int(mention['id']) - member = self.guild.get_member(id_search) - if member is not None: - self.mentions.append(member) - - def _handle_mention_roles(self, role_mentions): - self.role_mentions = [] - if self.guild is not None: - for role_id in map(int, role_mentions): - role = utils.get(self.guild.roles, id=role_id) - if role is not None: - self.role_mentions.append(role) - - def _handle_call(self, call): - if call is None or self.type is not MessageType.call: - self.call = None - return - - # we get the participant source from the mentions array or - # the author - - participants = [] - for uid in map(int, call.get('participants', [])): - if uid == self.author.id: - participants.append(self.author) - else: - user = utils.find(lambda u: u.id == uid, self.mentions) - if user is not None: - participants.append(user) - - call['participants'] = participants - self.call = CallMessage(message=self, **call) - - @utils.cached_slot_property('_cs_guild') - def guild(self): - """Optional[:class:`Guild`]: The guild that the message belongs to, if applicable.""" - return getattr(self.channel, 'guild', None) - - @utils.cached_slot_property('_cs_raw_mentions') - def raw_mentions(self): - """A property that returns an array of user IDs matched with - the syntax of <@user_id> in the message content. - - This allows you receive the user IDs of mentioned users - even in a private message context. - """ - return [int(x) for x in re.findall(r'<@!?([0-9]+)>', self.content)] - - @utils.cached_slot_property('_cs_raw_channel_mentions') - def raw_channel_mentions(self): - """A property that returns an array of channel IDs matched with - the syntax of <#channel_id> in the message content. - """ - return [int(x) for x in re.findall(r'<#([0-9]+)>', self.content)] - - @utils.cached_slot_property('_cs_raw_role_mentions') - def raw_role_mentions(self): - """A property that returns an array of role IDs matched with - the syntax of <@&role_id> in the message content. - """ - return [int(x) for x in re.findall(r'<@&([0-9]+)>', self.content)] - - @utils.cached_slot_property('_cs_channel_mentions') - def channel_mentions(self): - if self.guild is None: - return [] - it = filter(None, map(lambda m: self.guild.get_channel(m), self.raw_channel_mentions)) - return utils._unique(it) - - @utils.cached_slot_property('_cs_clean_content') - def clean_content(self): - """A property that returns the content in a "cleaned up" - manner. This basically means that mentions are transformed - into the way the client shows it. e.g. ``<#id>`` will transform - into ``#name``. - - This will also transform @everyone and @here mentions into - non-mentions. - """ - - transformations = { - re.escape('<#%s>' % channel.id): '#' + channel.name - for channel in self.channel_mentions - } - - mention_transforms = { - re.escape('<@%s>' % member.id): '@' + member.display_name - for member in self.mentions - } - - # add the <@!user_id> cases as well.. - second_mention_transforms = { - re.escape('<@!%s>' % member.id): '@' + member.display_name - for member in self.mentions - } - - transformations.update(mention_transforms) - transformations.update(second_mention_transforms) - - if self.guild is not None: - role_transforms = { - re.escape('<@&%s>' % role.id): '@' + role.name - for role in self.role_mentions - } - transformations.update(role_transforms) - - def repl(obj): - return transformations.get(re.escape(obj.group(0)), '') - - pattern = re.compile('|'.join(transformations.keys())) - result = pattern.sub(repl, self.content) - - transformations = { - '@everyone': '@\u200beveryone', - '@here': '@\u200bhere' - } - - def repl2(obj): - return transformations.get(obj.group(0), '') - - pattern = re.compile('|'.join(transformations.keys())) - return pattern.sub(repl2, result) - - @property - def created_at(self): - """datetime.datetime: The message's creation time in UTC.""" - return utils.snowflake_time(self.id) - - @property - def edited_at(self): - """Optional[datetime.datetime]: A naive UTC datetime object containing the edited time of the message.""" - return self._edited_timestamp - - @utils.cached_slot_property('_cs_system_content') - def system_content(self): - """A property that returns the content that is rendered - regardless of the :attr:`Message.type`. - - In the case of :attr:`MessageType.default`\, this just returns the - regular :attr:`Message.content`. Otherwise this returns an English - message denoting the contents of the system message. - """ - - if self.type is MessageType.default: - return self.content - - if self.type is MessageType.pins_add: - return '{0.name} pinned a message to this channel.'.format(self.author) - - if self.type is MessageType.recipient_add: - return '{0.name} added {1.name} to the group.'.format(self.author, self.mentions[0]) - - if self.type is MessageType.recipient_remove: - return '{0.name} removed {1.name} from the group.'.format(self.author, self.mentions[0]) - - if self.type is MessageType.channel_name_change: - return '{0.author.name} changed the channel name: {0.content}'.format(self) - - if self.type is MessageType.channel_icon_change: - return '{0.author.name} changed the channel icon.'.format(self) - - if self.type is MessageType.new_member: - formats = [ - "{0} just joined the server - glhf!", - "{0} just joined. Everyone, look busy!", - "{0} just joined. Can I get a heal?", - "{0} joined your party.", - "{0} joined. You must construct additional pylons.", - "Ermagherd. {0} is here.", - "Welcome, {0}. Stay awhile and listen.", - "Welcome, {0}. We were expecting you ( ͡° ͜ʖ ͡°)", - "Welcome, {0}. We hope you brought pizza.", - "Welcome {0}. Leave your weapons by the door.", - "A wild {0} appeared.", - "Swoooosh. {0} just landed.", - "Brace yourselves. {0} just joined the server.", - "{0} just joined. Hide your bananas.", - "{0} just arrived. Seems OP - please nerf.", - "{0} just slid into the server.", - "A {0} has spawned in the server.", - "Big {0} showed up!", - "Where’s {0}? In the server!", - "{0} hopped into the server. Kangaroo!!", - "{0} just showed up. Hold my beer.", - "Challenger approaching - {0} has appeared!", - "It's a bird! It's a plane! Nevermind, it's just {0}.", - "It's {0}! Praise the sun! [T]/", - "Never gonna give {0} up. Never gonna let {0} down.", - "Ha! {0} has joined! You activated my trap card!", - "Cheers, love! {0}'s here!", - "Hey! Listen! {0} has joined!", - "We've been expecting you {0}", - "It's dangerous to go alone, take {0}!", - "{0} has joined the server! It's super effective!", - "Cheers, love! {0} is here!", - "{0} is here, as the prophecy foretold.", - "{0} has arrived. Party's over.", - "Ready player {0}", - "{0} is here to kick butt and chew bubblegum. And {0} is all out of gum.", - "Hello. Is it {0} you're looking for?", - "{0} has joined. Stay a while and listen!", - "Roses are red, violets are blue, {0} joined this server with you", - ] - - index = int(self.created_at.timestamp()) % len(formats) - return formats[index].format(self.author.name) - - if self.type is MessageType.call: - # we're at the call message type now, which is a bit more complicated. - # we can make the assumption that Message.channel is a PrivateChannel - # with the type ChannelType.group or ChannelType.private - call_ended = self.call.ended_timestamp is not None - - if self.channel.me in self.call.participants: - return '{0.author.name} started a call.'.format(self) - elif call_ended: - return 'You missed a call from {0.author.name}'.format(self) - else: - return '{0.author.name} started a call \N{EM DASH} Join the call.'.format(self) - - @asyncio.coroutine - def delete(self): - """|coro| - - Deletes the message. - - Your own messages could be deleted without any proper permissions. However to - delete other people's messages, you need the :attr:`~Permissions.manage_messages` - permission. - - Raises - ------ - Forbidden - You do not have proper permissions to delete the message. - HTTPException - Deleting the message failed. - """ - yield from self._state.http.delete_message(self.channel.id, self.id) - - @asyncio.coroutine - def edit(self, **fields): - """|coro| - - Edits the message. - - The content must be able to be transformed into a string via ``str(content)``. - - Parameters - ----------- - content: Optional[str] - The new content to replace the message with. - Could be ``None`` to remove the content. - embed: Optional[:class:`Embed`] - The new embed to replace the original with. - Could be ``None`` to remove the embed. - delete_after: Optional[float] - If provided, the number of seconds to wait in the background - before deleting the message we just edited. If the deletion fails, - then it is silently ignored. - - Raises - ------- - HTTPException - Editing the message failed. - """ - - try: - content = fields['content'] - except KeyError: - pass - else: - if content is not None: - fields['content'] = str(content) - - try: - embed = fields['embed'] - except KeyError: - pass - else: - if embed is not None: - fields['embed'] = embed.to_dict() - - data = yield from self._state.http.edit_message(self.id, self.channel.id, **fields) - self._update(channel=self.channel, data=data) - - try: - delete_after = fields['delete_after'] - except KeyError: - pass - else: - if delete_after is not None: - @asyncio.coroutine - def delete(): - yield from asyncio.sleep(delete_after, loop=self._state.loop) - try: - yield from self._state.http.delete_message(self.channel.id, self.id) - except: - pass - - compat.create_task(delete(), loop=self._state.loop) - - @asyncio.coroutine - def pin(self): - """|coro| - - Pins the message. You must have :attr:`~Permissions.manage_messages` - permissions to do this in a non-private channel context. - - Raises - ------- - Forbidden - You do not have permissions to pin the message. - NotFound - The message or channel was not found or deleted. - HTTPException - Pinning the message failed, probably due to the channel - having more than 50 pinned messages. - """ - - yield from self._state.http.pin_message(self.channel.id, self.id) - self.pinned = True - - @asyncio.coroutine - def unpin(self): - """|coro| - - Unpins the message. You must have :attr:`~Permissions.manage_messages` - permissions to do this in a non-private channel context. - - Raises - ------- - Forbidden - You do not have permissions to unpin the message. - NotFound - The message or channel was not found or deleted. - HTTPException - Unpinning the message failed. - """ - - yield from self._state.http.unpin_message(self.channel.id, self.id) - self.pinned = False - - @asyncio.coroutine - def add_reaction(self, emoji): - """|coro| - - Add a reaction to the message. - - The emoji may be a unicode emoji or a custom guild :class:`Emoji`. - - You must have the :attr:`~Permissions.add_reactions` and - :attr:`~Permissions.read_message_history` permissions to use this. - - Parameters - ------------ - emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialReactionEmoji`, str] - The emoji to react with. - - Raises - -------- - HTTPException - Adding the reaction failed. - Forbidden - You do not have the proper permissions to react to the message. - NotFound - The emoji you specified was not found. - InvalidArgument - The emoji parameter is invalid. - """ - - if isinstance(emoji, Reaction): - emoji = emoji.emoji - - if isinstance(emoji, Emoji): - emoji = '%s:%s' % (emoji.name, emoji.id) - elif isinstance(emoji, PartialReactionEmoji): - emoji = emoji._as_reaction() - elif isinstance(emoji, str): - pass # this is okay - else: - raise InvalidArgument('emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.'.format(emoji)) - - yield from self._state.http.add_reaction(self.id, self.channel.id, emoji) - - @asyncio.coroutine - def remove_reaction(self, emoji, member): - """|coro| - - Remove a reaction by the member from the message. - - The emoji may be a unicode emoji or a custom guild :class:`Emoji`. - - If the reaction is not your own (i.e. ``member`` parameter is not you) then - the :attr:`~Permissions.manage_messages` permission is needed. - - The ``member`` parameter must represent a member and meet - the :class:`abc.Snowflake` abc. - - Parameters - ------------ - emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialReactionEmoji`, str] - The emoji to remove. - member: :class:`abc.Snowflake` - The member for which to remove the reaction. - - Raises - -------- - HTTPException - Removing the reaction failed. - Forbidden - You do not have the proper permissions to remove the reaction. - NotFound - The member or emoji you specified was not found. - InvalidArgument - The emoji parameter is invalid. - """ - - if isinstance(emoji, Reaction): - emoji = emoji.emoji - - if isinstance(emoji, Emoji): - emoji = '%s:%s' % (emoji.name, emoji.id) - elif isinstance(emoji, PartialReactionEmoji): - emoji = emoji._as_reaction() - elif isinstance(emoji, str): - pass # this is okay - else: - raise InvalidArgument('emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.'.format(emoji)) - - yield from self._state.http.remove_reaction(self.id, self.channel.id, emoji, member.id) - - @asyncio.coroutine - def clear_reactions(self): - """|coro| - - Removes all the reactions from the message. - - You need :attr:`~Permissions.manage_messages` permission - to use this. - - Raises - -------- - HTTPException - Removing the reactions failed. - Forbidden - You do not have the proper permissions to remove all the reactions. - """ - yield from self._state.http.clear_reactions(self.id, self.channel.id) - - def ack(self): - """|coro| - - Marks this message as read. - - The user must not be a bot user. - - Raises - ------- - HTTPException - Acking failed. - ClientException - You must not be a bot user. - """ - - state = self._state - if state.is_bot: - raise ClientException('Must not be a bot account to ack messages.') - return state.http.ack_message(self.channel.id, self.id) diff --git a/discord.py-rewrite/discord/mixins.py b/discord.py-rewrite/discord/mixins.py deleted file mode 100644 index 8dd1cef..0000000 --- a/discord.py-rewrite/discord/mixins.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -class EqualityComparable: - __slots__ = () - - def __eq__(self, other): - return isinstance(other, self.__class__) and other.id == self.id - - def __ne__(self, other): - if isinstance(other, self.__class__): - return other.id != self.id - return True - -class Hashable(EqualityComparable): - __slots__ = () - - def __hash__(self): - return self.id >> 22 diff --git a/discord.py-rewrite/discord/object.py b/discord.py-rewrite/discord/object.py deleted file mode 100644 index 9cb40ec..0000000 --- a/discord.py-rewrite/discord/object.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from . import utils -from .mixins import Hashable - -class Object(Hashable): - """Represents a generic Discord object. - - The purpose of this class is to allow you to create 'miniature' - versions of data classes if you want to pass in just an ID. Most functions - that take in a specific data class with an ID can also take in this class - as a substitute instead. Note that even though this is the case, not all - objects (if any) actually inherit from this class. - - There are also some cases where some websocket events are received - in :issue:`strange order <21>` and when such events happened you would - receive this class rather than the actual data class. These cases are - extremely rare. - - .. container:: operations - - .. describe:: x == y - - Checks if two objects are equal. - - .. describe:: x != y - - Checks if two objects are not equal. - - .. describe:: hash(x) - - Returns the object's hash. - - Attributes - ----------- - id : str - The ID of the object. - """ - - def __init__(self, id): - self.id = id - - @property - def created_at(self): - """Returns the snowflake's creation time in UTC.""" - return utils.snowflake_time(self.id) diff --git a/discord.py-rewrite/discord/opus.py b/discord.py-rewrite/discord/opus.py deleted file mode 100644 index 0054da4..0000000 --- a/discord.py-rewrite/discord/opus.py +++ /dev/null @@ -1,278 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import ctypes -import ctypes.util -import array -from .errors import DiscordException -import logging -import sys -import os.path - -log = logging.getLogger(__name__) -c_int_ptr = ctypes.POINTER(ctypes.c_int) -c_int16_ptr = ctypes.POINTER(ctypes.c_int16) -c_float_ptr = ctypes.POINTER(ctypes.c_float) - -class EncoderStruct(ctypes.Structure): - pass - -EncoderStructPtr = ctypes.POINTER(EncoderStruct) - -# A list of exported functions. -# The first argument is obviously the name. -# The second one are the types of arguments it takes. -# The third is the result type. -exported_functions = [ - ('opus_strerror', [ctypes.c_int], ctypes.c_char_p), - ('opus_encoder_get_size', [ctypes.c_int], ctypes.c_int), - ('opus_encoder_create', [ctypes.c_int, ctypes.c_int, ctypes.c_int, c_int_ptr], EncoderStructPtr), - ('opus_encode', [EncoderStructPtr, c_int16_ptr, ctypes.c_int, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int32), - ('opus_encoder_ctl', None, ctypes.c_int32), - ('opus_encoder_destroy', [EncoderStructPtr], None) -] - -def libopus_loader(name): - # create the library... - lib = ctypes.cdll.LoadLibrary(name) - - # register the functions... - for item in exported_functions: - try: - func = getattr(lib, item[0]) - except Exception as e: - raise e - - try: - if item[1]: - func.argtypes = item[1] - - func.restype = item[2] - except KeyError: - pass - - return lib - -try: - if sys.platform == 'win32': - _basedir = os.path.dirname(os.path.abspath(__file__)) - _bitness = 'x64' if sys.maxsize > 2**32 else 'x86' - _filename = os.path.join(_basedir, 'bin', 'libopus-0.{}.dll'.format(_bitness)) - _lib = libopus_loader(_filename) - else: - _lib = libopus_loader(ctypes.util.find_library('opus')) -except Exception as e: - _lib = None - -def load_opus(name): - """Loads the libopus shared library for use with voice. - - If this function is not called then the library uses the function - `ctypes.util.find_library`__ and then loads that one - if available. - - .. _find library: https://docs.python.org/3.5/library/ctypes.html#finding-shared-libraries - __ `find library`_ - - Not loading a library leads to voice not working. - - This function propagates the exceptions thrown. - - Warning - -------- - The bitness of the library must match the bitness of your python - interpreter. If the library is 64-bit then your python interpreter - must be 64-bit as well. Usually if there's a mismatch in bitness then - the load will throw an exception. - - Note - ---- - On Windows, the .dll extension is not necessary. However, on Linux - the full extension is required to load the library, e.g. ``libopus.so.1``. - On Linux however, `find library`_ will usually find the library automatically - without you having to call this. - - Parameters - ---------- - name: str - The filename of the shared library. - """ - global _lib - _lib = libopus_loader(name) - -def is_loaded(): - """Function to check if opus lib is successfully loaded either - via the ``ctypes.util.find_library`` call of :func:`load_opus`. - - This must return ``True`` for voice to work. - - Returns - ------- - bool - Indicates if the opus library has been loaded. - """ - global _lib - return _lib is not None - -class OpusError(DiscordException): - """An exception that is thrown for libopus related errors. - - Attributes - ---------- - code : int - The error code returned. - """ - - def __init__(self, code): - self.code = code - msg = _lib.opus_strerror(self.code).decode('utf-8') - log.info('"%s" has happened', msg) - super().__init__(msg) - -class OpusNotLoaded(DiscordException): - """An exception that is thrown for when libopus is not loaded.""" - pass - - -# Some constants... -OK = 0 -APPLICATION_AUDIO = 2049 -APPLICATION_VOIP = 2048 -APPLICATION_LOWDELAY = 2051 -CTL_SET_BITRATE = 4002 -CTL_SET_BANDWIDTH = 4008 -CTL_SET_FEC = 4012 -CTL_SET_PLP = 4014 -CTL_SET_SIGNAL = 4024 - -band_ctl = { - 'narrow': 1101, - 'medium': 1102, - 'wide': 1103, - 'superwide': 1104, - 'full': 1105, -} - -signal_ctl = { - 'auto': -1000, - 'voice': 3001, - 'music': 3002, -} - -class Encoder: - SAMPLING_RATE = 48000 - CHANNELS = 2 - FRAME_LENGTH = 20 - SAMPLE_SIZE = 4 # (bit_rate / 8) * CHANNELS (bit_rate == 16) - SAMPLES_PER_FRAME = int(SAMPLING_RATE / 1000 * FRAME_LENGTH) - - FRAME_SIZE = SAMPLES_PER_FRAME * SAMPLE_SIZE - - def __init__(self, application=APPLICATION_AUDIO): - self.application = application - - if not is_loaded(): - raise OpusNotLoaded() - - self._state = self._create_state() - self.set_bitrate(128) - self.set_fec(True) - self.set_expected_packet_loss_percent(0.15) - self.set_bandwidth('full') - self.set_signal_type('auto') - - def __del__(self): - if hasattr(self, '_state'): - _lib.opus_encoder_destroy(self._state) - self._state = None - - def _create_state(self): - ret = ctypes.c_int() - result = _lib.opus_encoder_create(self.SAMPLING_RATE, self.CHANNELS, self.application, ctypes.byref(ret)) - - if ret.value != 0: - log.info('error has happened in state creation') - raise OpusError(ret.value) - - return result - - def set_bitrate(self, kbps): - kbps = min(128, max(16, int(kbps))) - - ret = _lib.opus_encoder_ctl(self._state, CTL_SET_BITRATE, kbps * 1024) - if ret < 0: - log.info('error has happened in set_bitrate') - raise OpusError(ret) - - return kbps - - def set_bandwidth(self, req): - if req not in band_ctl: - raise KeyError('%r is not a valid bandwidth setting. Try one of: %s' % (req, ','.join(band_ctl))) - - k = band_ctl[req] - ret = _lib.opus_encoder_ctl(self._state, CTL_SET_BANDWIDTH, k) - - if ret < 0: - log.info('error has happened in set_bandwidth') - raise OpusError(ret) - - def set_signal_type(self, req): - if req not in signal_ctl: - raise KeyError('%r is not a valid signal setting. Try one of: %s' % (req, ','.join(signal_ctl))) - - k = signal_ctl[req] - ret = _lib.opus_encoder_ctl(self._state, CTL_SET_SIGNAL, k) - - if ret < 0: - log.info('error has happened in set_signal_type') - raise OpusError(ret) - - def set_fec(self, enabled=True): - ret = _lib.opus_encoder_ctl(self._state, CTL_SET_FEC, 1 if enabled else 0) - - if ret < 0: - log.info('error has happened in set_fec') - raise OpusError(ret) - - def set_expected_packet_loss_percent(self, percentage): - ret = _lib.opus_encoder_ctl(self._state, CTL_SET_PLP, min(100, max(0, int(percentage * 100)))) - - if ret < 0: - log.info('error has happened in set_expected_packet_loss_percent') - raise OpusError(ret) - - def encode(self, pcm, frame_size): - max_data_bytes = len(pcm) - pcm = ctypes.cast(pcm, c_int16_ptr) - data = (ctypes.c_char * max_data_bytes)() - - ret = _lib.opus_encode(self._state, pcm, frame_size, data, max_data_bytes) - if ret < 0: - log.info('error has happened in encode') - raise OpusError(ret) - - return array.array('b', data[:ret]).tobytes() diff --git a/discord.py-rewrite/discord/permissions.py b/discord.py-rewrite/discord/permissions.py deleted file mode 100644 index 12434c7..0000000 --- a/discord.py-rewrite/discord/permissions.py +++ /dev/null @@ -1,608 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -class Permissions: - """Wraps up the Discord permission value. - - The properties provided are two way. You can set and retrieve individual - bits using the properties as if they were regular bools. This allows - you to edit permissions. - - .. container:: operations - - .. describe:: x == y - - Checks if two permissions are equal. - .. describe:: x != y - - Checks if two permissions are not equal. - .. describe:: x <= y - - Checks if a permission is a subset of another permission. - .. describe:: x >= y - - Checks if a permission is a superset of another permission. - .. describe:: x < y - - Checks if a permission is a strict subset of another permission. - .. describe:: x > y - - Checks if a permission is a strict superset of another permission. - .. describe:: hash(x) - - Return the permission's hash. - .. describe:: iter(x) - - Returns an iterator of ``(perm, value)`` pairs. This allows it - to be, for example, constructed as a dict or a list of pairs. - - Attributes - ----------- - value - The raw value. This value is a bit array field of a 53-bit integer - representing the currently available permissions. You should query - permissions via the properties rather than using this raw value. - """ - - __slots__ = ('value',) - def __init__(self, permissions=0): - if not isinstance(permissions, int): - raise TypeError('Expected int parameter, received %s instead.' % permissions.__class__.__name__) - - self.value = permissions - - def __eq__(self, other): - return isinstance(other, Permissions) and self.value == other.value - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return hash(self.value) - - def __repr__(self): - return '' % self.value - - def _perm_iterator(self): - for attr in dir(self): - # check if it's a property, because if so it's a permission - is_property = isinstance(getattr(self.__class__, attr), property) - if is_property: - yield (attr, getattr(self, attr)) - - def __iter__(self): - return self._perm_iterator() - - def is_subset(self, other): - """Returns True if self has the same or fewer permissions as other.""" - if isinstance(other, Permissions): - return (self.value & other.value) == self.value - else: - raise TypeError("cannot compare {} with {}".format(self.__class__.__name__, other.__class__name)) - - def is_superset(self, other): - """Returns True if self has the same or more permissions as other.""" - if isinstance(other, Permissions): - return (self.value | other.value) == self.value - else: - raise TypeError("cannot compare {} with {}".format(self.__class__.__name__, other.__class__name)) - - def is_strict_subset(self, other): - """Returns True if the permissions on other are a strict subset of those on self.""" - return self.is_subset(other) and self != other - - def is_strict_superset(self, other): - """Returns True if the permissions on other are a strict superset of those on self.""" - return self.is_superset(other) and self != other - - __le__ = is_subset - __ge__ = is_superset - __lt__ = is_strict_subset - __gt__ = is_strict_superset - - @classmethod - def none(cls): - """A factory method that creates a :class:`Permissions` with all - permissions set to False.""" - return cls(0) - - @classmethod - def all(cls): - """A factory method that creates a :class:`Permissions` with all - permissions set to True.""" - return cls(0b01111111111101111111110011111111) - - @classmethod - def all_channel(cls): - """A :class:`Permissions` with all channel-specific permissions set to - True and the guild-specific ones set to False. The guild-specific - permissions are currently: - - - manage_guild - - kick_members - - ban_members - - administrator - - change_nicknames - - manage_nicknames - """ - return cls(0b00110011111101111111110001010001) - - @classmethod - def general(cls): - """A factory method that creates a :class:`Permissions` with all - "General" permissions from the official Discord UI set to True.""" - return cls(0b01111100000000000000000010111111) - - @classmethod - def text(cls): - """A factory method that creates a :class:`Permissions` with all - "Text" permissions from the official Discord UI set to True.""" - return cls(0b00000000000001111111110001000000) - - @classmethod - def voice(cls): - """A factory method that creates a :class:`Permissions` with all - "Voice" permissions from the official Discord UI set to True.""" - return cls(0b00000011111100000000000000000000) - - def update(self, **kwargs): - """Bulk updates this permission object. - - Allows you to set multiple attributes by using keyword - arguments. The names must be equivalent to the properties - listed. Extraneous key/value pairs will be silently ignored. - - Parameters - ------------ - \*\*kwargs - A list of key/value pairs to bulk update permissions with. - """ - for key, value in kwargs.items(): - try: - is_property = isinstance(getattr(self.__class__, key), property) - except AttributeError: - continue - - if is_property: - setattr(self, key, value) - - def _bit(self, index): - return bool((self.value >> index) & 1) - - def _set(self, index, value): - if value == True: - self.value |= (1 << index) - elif value == False: - self.value &= ~(1 << index) - else: - raise TypeError('Value to set for Permissions must be a bool.') - - def handle_overwrite(self, allow, deny): - # Basically this is what's happening here. - # We have an original bit array, e.g. 1010 - # Then we have another bit array that is 'denied', e.g. 1111 - # And then we have the last one which is 'allowed', e.g. 0101 - # We want original OP denied to end up resulting in - # whatever is in denied to be set to 0. - # So 1010 OP 1111 -> 0000 - # Then we take this value and look at the allowed values. - # And whatever is allowed is set to 1. - # So 0000 OP2 0101 -> 0101 - # The OP is base & ~denied. - # The OP2 is base | allowed. - self.value = (self.value & ~deny) | allow - - @property - def create_instant_invite(self): - """Returns True if the user can create instant invites.""" - return self._bit(0) - - @create_instant_invite.setter - def create_instant_invite(self, value): - self._set(0, value) - - @property - def kick_members(self): - """Returns True if the user can kick users from the guild.""" - return self._bit(1) - - @kick_members.setter - def kick_members(self, value): - self._set(1, value) - - @property - def ban_members(self): - """Returns True if a user can ban users from the guild.""" - return self._bit(2) - - @ban_members.setter - def ban_members(self, value): - self._set(2, value) - - @property - def administrator(self): - """Returns True if a user is an administrator. This role overrides all other permissions. - - This also bypasses all channel-specific overrides. - """ - return self._bit(3) - - @administrator.setter - def administrator(self, value): - self._set(3, value) - - @property - def manage_channels(self): - """Returns True if a user can edit, delete, or create channels in the guild. - - This also corresponds to the "manage channel" channel-specific override.""" - return self._bit(4) - - @manage_channels.setter - def manage_channels(self, value): - self._set(4, value) - - @property - def manage_guild(self): - """Returns True if a user can edit guild properties.""" - return self._bit(5) - - @manage_guild.setter - def manage_guild(self, value): - self._set(5, value) - - @property - def add_reactions(self): - """Returns True if a user can add reactions to messages.""" - return self._bit(6) - - @add_reactions.setter - def add_reactions(self, value): - self._set(6, value) - - @property - def view_audit_log(self): - """Returns True if a user can view the guild's audit log.""" - return self._bit(7) - - @view_audit_log.setter - def view_audit_log(self, value): - self._set(7, value) - - # 2 unused - - @property - def read_messages(self): - """Returns True if a user can read messages from all or specific text channels.""" - return self._bit(10) - - @read_messages.setter - def read_messages(self, value): - self._set(10, value) - - @property - def send_messages(self): - """Returns True if a user can send messages from all or specific text channels.""" - return self._bit(11) - - @send_messages.setter - def send_messages(self, value): - self._set(11, value) - - @property - def send_tts_messages(self): - """Returns True if a user can send TTS messages from all or specific text channels.""" - return self._bit(12) - - @send_tts_messages.setter - def send_tts_messages(self, value): - self._set(12, value) - - @property - def manage_messages(self): - """Returns True if a user can delete or pin messages in a text channel. Note that there are currently no ways to edit other people's messages.""" - return self._bit(13) - - @manage_messages.setter - def manage_messages(self, value): - self._set(13, value) - - @property - def embed_links(self): - """Returns True if a user's messages will automatically be embedded by Discord.""" - return self._bit(14) - - @embed_links.setter - def embed_links(self, value): - self._set(14, value) - - @property - def attach_files(self): - """Returns True if a user can send files in their messages.""" - return self._bit(15) - - @attach_files.setter - def attach_files(self, value): - self._set(15, value) - - @property - def read_message_history(self): - """Returns True if a user can read a text channel's previous messages.""" - return self._bit(16) - - @read_message_history.setter - def read_message_history(self, value): - self._set(16, value) - - @property - def mention_everyone(self): - """Returns True if a user's @everyone will mention everyone in the text channel.""" - return self._bit(17) - - @mention_everyone.setter - def mention_everyone(self, value): - self._set(17, value) - - @property - def external_emojis(self): - """Returns True if a user can use emojis from other guilds.""" - return self._bit(18) - - @external_emojis.setter - def external_emojis(self, value): - self._set(18, value) - - # 1 unused - - @property - def connect(self): - """Returns True if a user can connect to a voice channel.""" - return self._bit(20) - - @connect.setter - def connect(self, value): - self._set(20, value) - - @property - def speak(self): - """Returns True if a user can speak in a voice channel.""" - return self._bit(21) - - @speak.setter - def speak(self, value): - self._set(21, value) - - @property - def mute_members(self): - """Returns True if a user can mute other users.""" - return self._bit(22) - - @mute_members.setter - def mute_members(self, value): - self._set(22, value) - - @property - def deafen_members(self): - """Returns True if a user can deafen other users.""" - return self._bit(23) - - @deafen_members.setter - def deafen_members(self, value): - self._set(23, value) - - @property - def move_members(self): - """Returns True if a user can move users between other voice channels.""" - return self._bit(24) - - @move_members.setter - def move_members(self, value): - self._set(24, value) - - @property - def use_voice_activation(self): - """Returns True if a user can use voice activation in voice channels.""" - return self._bit(25) - - @use_voice_activation.setter - def use_voice_activation(self, value): - self._set(25, value) - - @property - def change_nickname(self): - """Returns True if a user can change their nickname in the guild.""" - return self._bit(26) - - @change_nickname.setter - def change_nickname(self, value): - self._set(26, value) - - @property - def manage_nicknames(self): - """Returns True if a user can change other user's nickname in the guild.""" - return self._bit(27) - - @manage_nicknames.setter - def manage_nicknames(self, value): - self._set(27, value) - - @property - def manage_roles(self): - """Returns True if a user can create or edit roles less than their role's position. - - This also corresponds to the "manage permissions" channel-specific override. - """ - return self._bit(28) - - @manage_roles.setter - def manage_roles(self, value): - self._set(28, value) - - @property - def manage_webhooks(self): - """Returns True if a user can create, edit, or delete webhooks.""" - return self._bit(29) - - @manage_webhooks.setter - def manage_webhooks(self, value): - self._set(29, value) - - @property - def manage_emojis(self): - """Returns True if a user can create, edit, or delete emojis.""" - return self._bit(30) - - @manage_emojis.setter - def manage_emojis(self, value): - self._set(30, value) - - # 1 unused - - # after these 32 bits, there's 21 more unused ones technically - -def augment_from_permissions(cls): - cls.VALID_NAMES = { name for name in dir(Permissions) if isinstance(getattr(Permissions, name), property) } - - # make descriptors for all the valid names - for name in cls.VALID_NAMES: - # god bless Python - def getter(self, x=name): - return self._values.get(x) - def setter(self, value, x=name): - self._set(x, value) - - prop = property(getter, setter) - setattr(cls, name, prop) - - return cls - -@augment_from_permissions -class PermissionOverwrite: - """A type that is used to represent a channel specific permission. - - Unlike a regular :class:`Permissions`\, the default value of a - permission is equivalent to ``None`` and not ``False``. Setting - a value to ``False`` is **explicitly** denying that permission, - while setting a value to ``True`` is **explicitly** allowing - that permission. - - The values supported by this are the same as :class:`Permissions` - with the added possibility of it being set to ``None``. - - Supported operations: - - +-----------+------------------------------------------+ - | Operation | Description | - +===========+==========================================+ - | iter(x) | Returns an iterator of (perm, value) | - | | pairs. This allows this class to be used | - | | as an iterable in e.g. set/list/dict | - | | constructions. | - +-----------+------------------------------------------+ - - Parameters - ----------- - \*\*kwargs - Set the value of permissions by their name. - """ - - __slots__ = ('_values',) - - def __init__(self, **kwargs): - self._values = {} - - for key, value in kwargs.items(): - if key not in self.VALID_NAMES: - raise ValueError('no permission called {0}.'.format(key)) - - setattr(self, key, value) - - def _set(self, key, value): - if value not in (True, None, False): - raise TypeError('Expected bool or NoneType, received {0.__class__.__name__}'.format(value)) - - self._values[key] = value - - def pair(self): - """Returns the (allow, deny) pair from this overwrite. - - The value of these pairs is :class:`Permissions`. - """ - - allow = Permissions.none() - deny = Permissions.none() - - for key, value in self._values.items(): - if value is True: - setattr(allow, key, True) - elif value is False: - setattr(deny, key, True) - - return allow, deny - - @classmethod - def from_pair(cls, allow, deny): - """Creates an overwrite from an allow/deny pair of :class:`Permissions`.""" - ret = cls() - for key, value in allow: - if value is True: - setattr(ret, key, True) - - for key, value in deny: - if value is True: - setattr(ret, key, False) - - return ret - - def is_empty(self): - """Checks if the permission overwrite is currently empty. - - An empty permission overwrite is one that has no overwrites set - to True or False. - """ - return all(x is None for x in self._values.values()) - - def update(self, **kwargs): - """Bulk updates this permission overwrite object. - - Allows you to set multiple attributes by using keyword - arguments. The names must be equivalent to the properties - listed. Extraneous key/value pairs will be silently ignored. - - Parameters - ------------ - \*\*kwargs - A list of key/value pairs to bulk update with. - """ - for key, value in kwargs.items(): - if key not in self.VALID_NAMES: - continue - - setattr(self, key, value) - - def __iter__(self): - for key in self.VALID_NAMES: - yield key, self._values.get(key) diff --git a/discord.py-rewrite/discord/player.py b/discord.py-rewrite/discord/player.py deleted file mode 100644 index f8afdfa..0000000 --- a/discord.py-rewrite/discord/player.py +++ /dev/null @@ -1,330 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import threading -import subprocess -import audioop -import logging -import shlex -import time - -from .errors import ClientException -from .opus import Encoder as OpusEncoder - -log = logging.getLogger(__name__) - -__all__ = [ 'AudioSource', 'PCMAudio', 'FFmpegPCMAudio', 'PCMVolumeTransformer' ] - -class AudioSource: - """Represents an audio stream. - - The audio stream can be Opus encoded or not, however if the audio stream - is not Opus encoded then the audio format must be 16-bit 48KHz stereo PCM. - - .. warning:: - - The audio source reads are done in a separate thread. - """ - - def read(self): - """Reads 20ms worth of audio. - - Subclasses must implement this. - - If the audio is complete, then returning an empty *bytes-like* object - to signal this is the way to do so. - - If :meth:`is_opus` method returns ``True``, then it must return - 20ms worth of Opus encoded audio. Otherwise, it must be 20ms - worth of 16-bit 48KHz stereo PCM, which is about 3,840 bytes - per frame (20ms worth of audio). - - Returns - -------- - bytes - A bytes like object that represents the PCM or Opus data. - """ - raise NotImplementedError - - def is_opus(self): - """Checks if the audio source is already encoded in Opus. - - Defaults to ``False``. - """ - return False - - def cleanup(self): - """Called when clean-up is needed to be done. - - Useful for clearing buffer data or processes after - it is done playing audio. - """ - pass - - def __del__(self): - self.cleanup() - -class PCMAudio(AudioSource): - """Represents raw 16-bit 48KHz stereo PCM audio source. - - Attributes - ----------- - stream: file-like object - A file-like object that reads byte data representing raw PCM. - """ - def __init__(self, stream): - self.stream = stream - - def read(self): - ret = self.stream.read(OpusEncoder.FRAME_SIZE) - if len(ret) != OpusEncoder.FRAME_SIZE: - return b'' - return ret - -class FFmpegPCMAudio(AudioSource): - """An audio source from FFmpeg (or AVConv). - - This launches a sub-process to a specific input file given. - - .. warning:: - - You must have the ffmpeg or avconv executable in your path environment - variable in order for this to work. - - Parameters - ------------ - source: Union[str, BinaryIO] - The input that ffmpeg will take and convert to PCM bytes. - If ``pipe`` is True then this is a file-like object that is - passed to the stdin of ffmpeg. - executable: str - The executable name (and path) to use. Defaults to ``ffmpeg``. - pipe: bool - If true, denotes that ``source`` parameter will be passed - to the stdin of ffmpeg. Defaults to ``False``. - stderr: Optional[BinaryIO] - A file-like object to pass to the Popen constructor. - Could also be an instance of ``subprocess.PIPE``. - options: Optional[str] - Extra command line arguments to pass to ffmpeg after the ``-i`` flag. - before_options: Optional[str] - Extra command line arguments to pass to ffmpeg before the ``-i`` flag. - - Raises - -------- - ClientException - The subprocess failed to be created. - """ - - def __init__(self, source, *, executable='ffmpeg', pipe=False, stderr=None, before_options=None, options=None): - stdin = None if not pipe else source - - args = [executable] - - if isinstance(before_options, str): - args.extend(shlex.split(before_options)) - - args.append('-i') - args.append('-' if pipe else source) - args.extend(('-f', 's16le', '-ar', '48000', '-ac', '2', '-loglevel', 'warning')) - - if isinstance(options, str): - args.extend(shlex.split(options)) - - args.append('pipe:1') - - try: - self._process = subprocess.Popen(args, stdin=stdin, stdout=subprocess.PIPE, stderr=stderr) - self._stdout = self._process.stdout - except FileNotFoundError: - raise ClientException(executable + ' was not found.') from None - except subprocess.SubprocessError as e: - raise ClientException('Popen failed: {0.__class__.__name__}: {0}'.format(e)) from e - - def read(self): - ret = self._stdout.read(OpusEncoder.FRAME_SIZE) - if len(ret) != OpusEncoder.FRAME_SIZE: - return b'' - return ret - - def cleanup(self): - proc = self._process - if proc is None: - return - - log.info('Preparing to terminate ffmpeg process %s.', proc.pid) - proc.kill() - if proc.poll() is None: - log.info('ffmpeg process %s has not terminated. Waiting to terminate...', proc.pid) - proc.communicate() - log.info('ffmpeg process %s should have terminated with a return code of %s.', proc.pid, proc.returncode) - else: - log.info('ffmpeg process %s successfully terminated with return code of %s.', proc.pid, proc.returncode) - - self._process = None - -class PCMVolumeTransformer(AudioSource): - """Transforms a previous :class:`AudioSource` to have volume controls. - - This does not work on audio sources that have :meth:`AudioSource.is_opus` - set to ``True``. - - Parameters - ------------ - original: :class:`AudioSource` - The original AudioSource to transform. - volume: float - The initial volume to set it to. - See :attr:`volume` for more info. - - Raises - ------- - TypeError - Not an audio source. - ClientException - The audio source is opus encoded. - """ - - def __init__(self, original, volume=1.0): - if not isinstance(original, AudioSource): - raise TypeError('expected AudioSource not {0.__class__.__name__}.'.format(original)) - - if original.is_opus(): - raise ClientException('AudioSource must not be Opus encoded.') - - self.original = original - self.volume = volume - - @property - def volume(self): - """Retrieves or sets the volume as a floating point percentage (e.g. 1.0 for 100%).""" - return self._volume - - @volume.setter - def volume(self, value): - self._volume = max(value, 0.0) - - def cleanup(self): - self.original.cleanup() - - def read(self): - ret = self.original.read() - return audioop.mul(ret, 2, min(self._volume, 2.0)) - -class AudioPlayer(threading.Thread): - DELAY = OpusEncoder.FRAME_LENGTH / 1000.0 - - def __init__(self, source, client, *, after=None): - threading.Thread.__init__(self) - self.daemon = True - self.source = source - self.client = client - self.after = after - - self._end = threading.Event() - self._resumed = threading.Event() - self._resumed.set() # we are not paused - self._current_error = None - self._connected = client._connected - self._lock = threading.Lock() - - if after is not None and not callable(after): - raise TypeError('Expected a callable for the "after" parameter.') - - def _do_run(self): - self.loops = 0 - self._start = time.time() - - # getattr lookup speed ups - play_audio = self.client.send_audio_packet - - while not self._end.is_set(): - # are we paused? - if not self._resumed.is_set(): - # wait until we aren't - self._resumed.wait() - continue - - # are we disconnected from voice? - if not self._connected.is_set(): - # wait until we are connected - self._connected.wait() - # reset our internal data - self.loops = 0 - self._start = time.time() - - self.loops += 1 - data = self.source.read() - - if not data: - self.stop() - break - - play_audio(data, encode=not self.source.is_opus()) - next_time = self._start + self.DELAY * self.loops - delay = max(0, self.DELAY + (next_time - time.time())) - time.sleep(delay) - - def run(self): - try: - self._do_run() - except Exception as e: - self._current_error = e - self.stop() - finally: - self.source.cleanup() - self._call_after() - - def _call_after(self): - if self.after is not None: - try: - self.after(self._current_error) - except: - log.exception('Calling the after function failed.') - - def stop(self): - self._end.set() - self._resumed.set() - - def pause(self): - self._resumed.clear() - - def resume(self): - self.loops = 0 - self._start = time.time() - self._resumed.set() - - def is_playing(self): - return self._resumed.is_set() and not self._end.is_set() - - def is_paused(self): - return not self._end.is_set() and not self._resumed.is_set() - - def _set_source(self, source): - with self._lock: - self.pause() - self.source = source - self.resume() diff --git a/discord.py-rewrite/discord/reaction.py b/discord.py-rewrite/discord/reaction.py deleted file mode 100644 index 5c7c362..0000000 --- a/discord.py-rewrite/discord/reaction.py +++ /dev/null @@ -1,163 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from .iterators import ReactionIterator - -class Reaction: - """Represents a reaction to a message. - - Depending on the way this object was created, some of the attributes can - have a value of ``None``. - - .. container:: operations - - .. describe:: x == y - - Checks if two reactions are equal. This works by checking if the emoji - is the same. So two messages with the same reaction will be considered - "equal". - - .. describe:: x != y - - Checks if two reactions are not equal. - - .. describe:: hash(x) - - Returns the reaction's hash. - - .. describe:: str(x) - - Returns the string form of the reaction's emoji. - - Attributes - ----------- - emoji: :class:`Emoji` or str - The reaction emoji. May be a custom emoji, or a unicode emoji. - count: int - Number of times this reaction was made - me: bool - If the user sent this reaction. - message: :class:`Message` - Message this reaction is for. - """ - __slots__ = ('message', 'count', 'emoji', 'me') - - def __init__(self, *, message, data, emoji=None): - self.message = message - self.emoji = emoji or message._state.get_reaction_emoji(data['emoji']) - self.count = data.get('count', 1) - self.me = data.get('me') - - @property - def custom_emoji(self): - """bool: If this is a custom emoji.""" - return not isinstance(self.emoji, str) - - def __eq__(self, other): - return isinstance(other, self.__class__) and other.emoji == self.emoji - - def __ne__(self, other): - if isinstance(other, self.__class__): - return other.emoji != self.emoji - return True - - def __hash__(self): - return hash(self.emoji) - - def __str__(self): - return str(self.emoji) - - def __repr__(self): - return ''.format(self) - - def users(self, limit=None, after=None): - """|coro| - - Returns an :class:`AsyncIterator` representing the - users that have reacted to the message. - - The ``after`` parameter must represent a member - and meet the :class:`abc.Snowflake` abc. - - Parameters - ------------ - limit: int - The maximum number of results to return. - If not provided, returns all the users who - reacted to the message. - after: :class:`abc.Snowflake` - For pagination, reactions are sorted by member. - - Raises - -------- - HTTPException - Getting the users for the reaction failed. - - Examples - --------- - - Usage :: - - # I do not actually recommend doing this. - async for user in reaction.users(): - await channel.send('{0} has reacted with {1.emoji}!'.format(user, reaction)) - - Flattening into a list: :: - - users = await reaction.users().flatten() - # users is now a list... - winner = random.choice(users) - await channel.send('{} has won the raffle.'.format(winner)) - - Python 3.4 Usage :: - - iterator = reaction.users() - while True: - try: - user = yield from iterator.next() - except discord.NoMoreItems: - break - else: - await channel.send('{0} has reacted with {1.emoji}!'.format(user, reaction)) - - Yields - -------- - Union[:class:`User`, :class:`Member`] - The member (if retrievable) or the user that has reacted - to this message. The case where it can be a :class:`Member` is - in a guild message context. Sometimes it can be a :class:`User` - if the member has left the guild. - """ - - if self.custom_emoji: - emoji = '{0.name}:{0.id}'.format(self.emoji) - else: - emoji = self.emoji - - if limit is None: - limit = self.count - - return ReactionIterator(self.message, emoji, limit, after) diff --git a/discord.py-rewrite/discord/relationship.py b/discord.py-rewrite/discord/relationship.py deleted file mode 100644 index 575627e..0000000 --- a/discord.py-rewrite/discord/relationship.py +++ /dev/null @@ -1,82 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from .enums import RelationshipType, try_enum - -import asyncio - -class Relationship: - """Represents a relationship in Discord. - - A relationship is like a friendship, a person who is blocked, etc. - Only non-bot accounts can have relationships. - - Attributes - ----------- - user: :class:`User` - The user you have the relationship with. - type: :class:`RelationshipType` - The type of relationship you have. - """ - - __slots__ = ('type', 'user', '_state') - - def __init__(self, *, state, data): - self._state = state - self.type = try_enum(RelationshipType, data['type']) - self.user = state.store_user(data['user']) - - def __repr__(self): - return ''.format(self) - - @asyncio.coroutine - def delete(self): - """|coro| - - Deletes the relationship. - - Raises - ------ - HTTPException - Deleting the relationship failed. - """ - - yield from self._state.http.remove_relationship(self.user.id) - - @asyncio.coroutine - def accept(self): - """|coro| - - Accepts the relationship request. e.g. accepting a - friend request. - - Raises - ------- - HTTPException - Accepting the relationship failed. - """ - - yield from self._state.http.add_relationship(self.user.id) diff --git a/discord.py-rewrite/discord/role.py b/discord.py-rewrite/discord/role.py deleted file mode 100644 index c4ccbf9..0000000 --- a/discord.py-rewrite/discord/role.py +++ /dev/null @@ -1,284 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import asyncio - -from .permissions import Permissions -from .errors import InvalidArgument -from .colour import Colour -from .mixins import Hashable -from .utils import snowflake_time - -class Role(Hashable): - """Represents a Discord role in a :class:`Guild`. - - .. container:: operations - - .. describe:: x == y - - Checks if two roles are equal. - - .. describe:: x != y - - Checks if two roles are not equal. - - .. describe:: x > y - - Checks if a role is higher than another in the hierarchy. - - .. describe:: x < y - - Checks if a role is lower than another in the hierarchy. - - .. describe:: x >= y - - Checks if a role is higher or equal to another in the hierarchy. - - .. describe:: x <= y - - Checks if a role is lower or equal to another in the hierarchy. - - .. describe:: hash(x) - - Return the role's hash. - - .. describe:: str(x) - - Returns the role's name. - - Attributes - ---------- - id: int - The ID for the role. - name: str - The name of the role. - permissions: :class:`Permissions` - Represents the role's permissions. - guild: :class:`Guild` - The guild the role belongs to. - colour: :class:`Colour` - Represents the role colour. An alias exists under ``color``. - hoist: bool - Indicates if the role will be displayed separately from other members. - position: int - The position of the role. This number is usually positive. The bottom - role has a position of 0. - managed: bool - Indicates if the role is managed by the guild through some form of - integrations such as Twitch. - mentionable: bool - Indicates if the role can be mentioned by users. - """ - - __slots__ = ('id', 'name', 'permissions', 'color', 'colour', 'position', - 'managed', 'mentionable', 'hoist', 'guild', '_state' ) - - def __init__(self, *, guild, state, data): - self.guild = guild - self._state = state - self.id = int(data['id']) - self._update(data) - - def __str__(self): - return self.name - - def __repr__(self): - return ''.format(self) - - def __lt__(self, other): - if not isinstance(other, Role) or not isinstance(self, Role): - return NotImplemented - - if self.guild != other.guild: - raise RuntimeError('cannot compare roles from two different guilds.') - - if self.position < other.position: - return True - - if self.position == other.position: - return int(self.id) > int(other.id) - - return False - - def __le__(self, other): - r = Role.__lt__(other, self) - if r is NotImplemented: - return NotImplemented - return not r - - def __gt__(self, other): - return Role.__lt__(other, self) - - def __ge__(self, other): - r = Role.__lt__(self, other) - if r is NotImplemented: - return NotImplemented - return not r - - def _update(self, data): - self.name = data['name'] - self.permissions = Permissions(data.get('permissions', 0)) - self.position = data.get('position', 0) - self.colour = Colour(data.get('color', 0)) - self.hoist = data.get('hoist', False) - self.managed = data.get('managed', False) - self.mentionable = data.get('mentionable', False) - self.color = self.colour - - def is_default(self): - """Checks if the role is the default role.""" - return self.guild.id == self.id - - @property - def created_at(self): - """Returns the role's creation time in UTC.""" - return snowflake_time(self.id) - - @property - def mention(self): - """Returns a string that allows you to mention a role.""" - return '<@&%s>' % self.id - - @property - def members(self): - """Returns a list of :class:`Member` with this role.""" - all_members = self.guild.members - if self.is_default(): - return all_members - - return [member for member in all_members if self in member.roles] - - @asyncio.coroutine - def _move(self, position, reason): - if position <= 0: - raise InvalidArgument("Cannot move role to position 0 or below") - - if self.is_default(): - raise InvalidArgument("Cannot move default role") - - if self.position == position: - return # Save discord the extra request. - - http = self._state.http - - change_range = range(min(self.position, position), max(self.position, position) + 1) - sorted_roles = sorted((x for x in self.guild.roles if x.position in change_range and x.id != self.id), - key=lambda x: x.position) - - roles = [r.id for r in sorted_roles] - - if self.position > position: - roles.insert(0, self.id) - else: - roles.append(self.id) - - payload = [{"id": z[0], "position": z[1]} for z in zip(roles, change_range)] - yield from http.move_role_position(self.guild.id, payload, reason=reason) - - @asyncio.coroutine - def edit(self, *, reason=None, **fields): - """|coro| - - Edits the role. - - You must have the :attr:`Permissions.manage_roles` permission to - use this. - - All fields are optional. - - Parameters - ----------- - name: str - The new role name to change to. - permissions: :class:`Permissions` - The new permissions to change to. - colour: :class:`Colour` - The new colour to change to. (aliased to color as well) - hoist: bool - Indicates if the role should be shown separately in the member list. - mentionable: bool - Indicates if the role should be mentionable by others. - position: int - The new role's position. This must be below your top role's - position or it will fail. - reason: Optional[str] - The reason for editing this role. Shows up on the audit log. - - Raises - ------- - Forbidden - You do not have permissions to change the role. - HTTPException - Editing the role failed. - InvalidArgument - An invalid position was given or the default - role was asked to be moved. - """ - - position = fields.get('position') - if position is not None: - yield from self._move(position, reason=reason) - self.position = position - - try: - colour = fields['colour'] - except KeyError: - colour = fields.get('color', self.colour) - - payload = { - 'name': fields.get('name', self.name), - 'permissions': fields.get('permissions', self.permissions).value, - 'color': colour.value, - 'hoist': fields.get('hoist', self.hoist), - 'mentionable': fields.get('mentionable', self.mentionable) - } - - data = yield from self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload) - self._update(data) - - @asyncio.coroutine - def delete(self, *, reason=None): - """|coro| - - Deletes the role. - - You must have the :attr:`Permissions.manage_roles` permission to - use this. - - Parameters - ----------- - reason: Optional[str] - The reason for deleting this role. Shows up on the audit log. - - Raises - -------- - Forbidden - You do not have permissions to delete the role. - HTTPException - Deleting the role failed. - """ - - yield from self._state.http.delete_role(self.guild.id, self.id, reason=reason) diff --git a/discord.py-rewrite/discord/shard.py b/discord.py-rewrite/discord/shard.py deleted file mode 100644 index 8946305..0000000 --- a/discord.py-rewrite/discord/shard.py +++ /dev/null @@ -1,346 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from .state import AutoShardedConnectionState -from .client import Client -from .gateway import * -from .errors import ClientException, InvalidArgument -from . import compat -from .enums import Status - -import asyncio -import logging -import websockets -import itertools - -log = logging.getLogger(__name__) - -class Shard: - def __init__(self, ws, client): - self.ws = ws - self._client = client - self.loop = self._client.loop - self._current = compat.create_future(self.loop) - self._current.set_result(None) # we just need an already done future - - @property - def id(self): - return self.ws.shard_id - - @asyncio.coroutine - def poll(self): - try: - yield from self.ws.poll_event() - except ResumeWebSocket as e: - log.info('Got a request to RESUME the websocket at Shard ID %s.', self.id) - coro = DiscordWebSocket.from_client(self._client, resume=True, - shard_id=self.id, - session=self.ws.session_id, - sequence=self.ws.sequence) - self.ws = yield from asyncio.wait_for(coro, timeout=180.0, loop=self.loop) - - def get_future(self): - if self._current.done(): - self._current = compat.create_task(self.poll(), loop=self.loop) - - return self._current - -@asyncio.coroutine -def _ensure_coroutine_connect(gateway, loop): - # In 3.5+ websockets.connect does not return a coroutine, but an awaitable. - # The problem is that in 3.5.0 and in some cases 3.5.1, asyncio.ensure_future and - # by proxy, asyncio.wait_for, do not accept awaitables, but rather futures or coroutines. - # By wrapping it up into this function we ensure that it's in a coroutine and not an awaitable - # even for 3.5.0 users. - ws = yield from websockets.connect(gateway, loop=loop, klass=DiscordWebSocket) - return ws - -class AutoShardedClient(Client): - """A client similar to :class:`Client` except it handles the complications - of sharding for the user into a more manageable and transparent single - process bot. - - When using this client, you will be able to use it as-if it was a regular - :class:`Client` with a single shard when implementation wise internally it - is split up into multiple shards. This allows you to not have to deal with - IPC or other complicated infrastructure. - - It is recommended to use this client only if you have surpassed at least - 1000 guilds. - - If no :attr:`shard_count` is provided, then the library will use the - Bot Gateway endpoint call to figure out how many shards to use. - - If a ``shard_ids`` parameter is given, then those shard IDs will be used - to launch the internal shards. Note that :attr:`shard_count` must be provided - if this is used. By default, when omitted, the client will launch shards from - 0 to ``shard_count - 1``. - - Attributes - ------------ - shard_ids: Optional[List[int]] - An optional list of shard_ids to launch the shards with. - """ - def __init__(self, *args, loop=None, **kwargs): - kwargs.pop('shard_id', None) - self.shard_ids = kwargs.pop('shard_ids', None) - super().__init__(*args, loop=loop, **kwargs) - - if self.shard_ids is not None: - if self.shard_count is None: - raise ClientException('When passing manual shard_ids, you must provide a shard_count.') - elif not isinstance(self.shard_ids, (list, tuple)): - raise ClientException('shard_ids parameter must be a list or a tuple.') - - self._connection = AutoShardedConnectionState(dispatch=self.dispatch, chunker=self._chunker, - syncer=self._syncer, http=self.http, loop=self.loop, **kwargs) - - # instead of a single websocket, we have multiple - # the key is the shard_id - self.shards = {} - - def _get_websocket(guild_id): - i = (guild_id >> 22) % self.shard_count - return self.shards[i].ws - - self._connection._get_websocket = _get_websocket - self._still_sharding = True - - @asyncio.coroutine - def _chunker(self, guild, *, shard_id=None): - try: - guild_id = guild.id - shard_id = shard_id or guild.shard_id - except AttributeError: - guild_id = [s.id for s in guild] - - payload = { - 'op': 8, - 'd': { - 'guild_id': guild_id, - 'query': '', - 'limit': 0 - } - } - - ws = self.shards[shard_id].ws - yield from ws.send_as_json(payload) - - @property - def latency(self): - """float: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. - - This operates similarly to :meth:`.Client.latency` except it uses the average - latency of every shard's latency. To get a list of shard latency, check the - :attr:`latencies` property. - """ - return sum(latency for _, latency in self.latencies) / len(self.shards) - - @property - def latencies(self): - """List[Tuple[int, float]]: A list of latencies between a HEARTBEAT and a HEARTBEAT_ACK in seconds. - - This returns a list of tuples with elements ``(shard_id, latency)``. - """ - return [(shard_id, shard.ws.latency) for shard_id, shard in self.shards.items()] - - @asyncio.coroutine - def request_offline_members(self, *guilds): - """|coro| - - Requests previously offline members from the guild to be filled up - into the :attr:`Guild.members` cache. This function is usually not - called. It should only be used if you have the ``fetch_offline_members`` - parameter set to ``False``. - - When the client logs on and connects to the websocket, Discord does - not provide the library with offline members if the number of members - in the guild is larger than 250. You can check if a guild is large - if :attr:`Guild.large` is ``True``. - - Parameters - ----------- - \*guilds - An argument list of guilds to request offline members for. - - Raises - ------- - InvalidArgument - If any guild is unavailable or not large in the collection. - """ - if any(not g.large or g.unavailable for g in guilds): - raise InvalidArgument('An unavailable or non-large guild was passed.') - - _guilds = sorted(guilds, key=lambda g: g.shard_id) - for shard_id, sub_guilds in itertools.groupby(_guilds, key=lambda g: g.shard_id): - sub_guilds = list(sub_guilds) - yield from self._connection.request_offline_members(sub_guilds, shard_id=shard_id) - - @asyncio.coroutine - def pending_reads(self, shard): - try: - while self._still_sharding: - yield from shard.poll() - except asyncio.CancelledError: - pass - - @asyncio.coroutine - def launch_shard(self, gateway, shard_id): - try: - ws = yield from asyncio.wait_for(_ensure_coroutine_connect(gateway, self.loop), loop=self.loop, timeout=180.0) - except Exception as e: - log.info('Failed to connect for shard_id: %s. Retrying...', shard_id) - yield from asyncio.sleep(5.0, loop=self.loop) - return (yield from self.launch_shard(gateway, shard_id)) - - ws.token = self.http.token - ws._connection = self._connection - ws._dispatch = self.dispatch - ws.gateway = gateway - ws.shard_id = shard_id - ws.shard_count = self.shard_count - ws._max_heartbeat_timeout = self._connection.heartbeat_timeout - - try: - # OP HELLO - yield from asyncio.wait_for(ws.poll_event(), loop=self.loop, timeout=180.0) - yield from asyncio.wait_for(ws.identify(), loop=self.loop, timeout=180.0) - except asyncio.TimeoutError: - log.info('Timed out when connecting for shard_id: %s. Retrying...', shard_id) - yield from asyncio.sleep(5.0, loop=self.loop) - return (yield from self.launch_shard(gateway, shard_id)) - - # keep reading the shard while others connect - self.shards[shard_id] = ret = Shard(ws, self) - compat.create_task(self.pending_reads(ret), loop=self.loop) - yield from asyncio.sleep(5.0, loop=self.loop) - - @asyncio.coroutine - def launch_shards(self): - if self.shard_count is None: - self.shard_count, gateway = yield from self.http.get_bot_gateway() - else: - gateway = yield from self.http.get_gateway() - - self._connection.shard_count = self.shard_count - - shard_ids = self.shard_ids if self.shard_ids else range(self.shard_count) - - for shard_id in shard_ids: - yield from self.launch_shard(gateway, shard_id) - - self._still_sharding = False - - @asyncio.coroutine - def _connect(self): - yield from self.launch_shards() - - while True: - pollers = [shard.get_future() for shard in self.shards.values()] - done, pending = yield from asyncio.wait(pollers, loop=self.loop, return_when=asyncio.FIRST_COMPLETED) - for f in done: - # we wanna re-raise to the main Client.connect handler if applicable - f.result() - - @asyncio.coroutine - def close(self): - """|coro| - - Closes the connection to discord. - """ - if self.is_closed(): - return - - self._closed.set() - - for vc in self.voice_clients: - try: - yield from vc.disconnect() - except: - pass - - to_close = [shard.ws.close() for shard in self.shards.values()] - yield from asyncio.wait(to_close, loop=self.loop) - yield from self.http.close() - - @asyncio.coroutine - def change_presence(self, *, game=None, status=None, afk=False, shard_id=None): - """|coro| - - Changes the client's presence. - - The game parameter is a Game object (not a string) that represents - a game being played currently. - - Parameters - ---------- - game: Optional[:class:`Game`] - The game being played. None if no game is being played. - status: Optional[:class:`Status`] - Indicates what status to change to. If None, then - :attr:`Status.online` is used. - afk: bool - Indicates if you are going AFK. This allows the discord - client to know how to handle push notifications better - for you in case you are actually idle and not lying. - shard_id: Optional[int] - The shard_id to change the presence to. If not specified - or ``None``, then it will change the presence of every - shard the bot can see. - - Raises - ------ - InvalidArgument - If the ``game`` parameter is not :class:`Game` or None. - """ - - if status is None: - status = 'online' - status_enum = Status.online - elif status is Status.offline: - status = 'invisible' - status_enum = Status.offline - else: - status_enum = status - status = str(status) - - if shard_id is None: - for shard in self.shards.values(): - yield from shard.ws.change_presence(game=game, status=status, afk=afk) - - guilds = self._connection.guilds - else: - shard = self.shards[shard_id] - yield from shard.ws.change_presence(game=game, status=status, afk=afk) - guilds = [g for g in self._connection.guilds if g.shard_id == shard_id] - - for guild in guilds: - me = guild.me - if me is None: - continue - - me.game = game - me.status = status_enum diff --git a/discord.py-rewrite/discord/state.py b/discord.py-rewrite/discord/state.py deleted file mode 100644 index b145c7e..0000000 --- a/discord.py-rewrite/discord/state.py +++ /dev/null @@ -1,955 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from .guild import Guild -from .user import User, ClientUser -from .emoji import Emoji, PartialReactionEmoji -from .message import Message -from .relationship import Relationship -from .channel import * -from .member import Member -from .role import Role -from .enums import ChannelType, try_enum, Status -from .calls import GroupCall -from . import utils, compat -from .embeds import Embed - -from collections import deque, namedtuple, OrderedDict -import copy, enum, math -import datetime -import asyncio -import logging -import weakref -import itertools - -class ListenerType(enum.Enum): - chunk = 0 - -Listener = namedtuple('Listener', ('type', 'future', 'predicate')) -log = logging.getLogger(__name__) -ReadyState = namedtuple('ReadyState', ('launch', 'guilds')) - -class ConnectionState: - def __init__(self, *, dispatch, chunker, syncer, http, loop, **options): - self.loop = loop - self.http = http - self.max_messages = max(options.get('max_messages', 5000), 100) - self.dispatch = dispatch - self.chunker = chunker - self.syncer = syncer - self.is_bot = None - self.shard_count = None - self._ready_task = None - self._fetch_offline = options.get('fetch_offline_members', True) - self.heartbeat_timeout = options.get('heartbeat_timeout', 60.0) - self._listeners = [] - - game = options.get('game', None) - if game: - game = dict(game) - - status = options.get('status', None) - if status: - if status is Status.offline: - status = 'invisible' - else: - status = str(status) - - self._game = game - self._status = status - - self.clear() - - def clear(self): - self.user = None - self._users = weakref.WeakValueDictionary() - self._emojis = {} - self._calls = {} - self._guilds = {} - self._voice_clients = {} - - # LRU of max size 128 - self._private_channels = OrderedDict() - # extra dict to look up private channels by user id - self._private_channels_by_user = {} - self._messages = deque(maxlen=self.max_messages) - - def process_listeners(self, listener_type, argument, result): - removed = [] - for i, listener in enumerate(self._listeners): - if listener.type != listener_type: - continue - - future = listener.future - if future.cancelled(): - removed.append(i) - continue - - try: - passed = listener.predicate(argument) - except Exception as e: - future.set_exception(e) - removed.append(i) - else: - if passed: - future.set_result(result) - removed.append(i) - if listener.type == ListenerType.chunk: - break - - for index in reversed(removed): - del self._listeners[index] - - @property - def self_id(self): - u = self.user - return u.id if u else None - - @property - def voice_clients(self): - return list(self._voice_clients.values()) - - def _get_voice_client(self, guild_id): - return self._voice_clients.get(guild_id) - - def _add_voice_client(self, guild_id, voice): - self._voice_clients[guild_id] = voice - - def _remove_voice_client(self, guild_id): - self._voice_clients.pop(guild_id, None) - - def _update_references(self, ws): - for vc in self.voice_clients: - vc.main_ws = ws - - def store_user(self, data): - # this way is 300% faster than `dict.setdefault`. - user_id = int(data['id']) - try: - return self._users[user_id] - except KeyError: - self._users[user_id] = user = User(state=self, data=data) - return user - - def get_user(self, id): - return self._users.get(id) - - def store_emoji(self, guild, data): - emoji_id = int(data['id']) - self._emojis[emoji_id] = emoji = Emoji(guild=guild, state=self, data=data) - return emoji - - @property - def guilds(self): - return list(self._guilds.values()) - - def _get_guild(self, guild_id): - return self._guilds.get(guild_id) - - def _add_guild(self, guild): - self._guilds[guild.id] = guild - - def _remove_guild(self, guild): - self._guilds.pop(guild.id, None) - - for emoji in guild.emojis: - self._emojis.pop(emoji.id, None) - - del guild - - @property - def emojis(self): - return list(self._emojis.values()) - - def get_emoji(self, emoji_id): - return self._emojis.get(emoji_id) - - @property - def private_channels(self): - return list(self._private_channels.values()) - - def _get_private_channel(self, channel_id): - try: - value = self._private_channels[channel_id] - except KeyError: - return None - else: - self._private_channels.move_to_end(channel_id) - return value - - def _get_private_channel_by_user(self, user_id): - return self._private_channels_by_user.get(user_id) - - def _add_private_channel(self, channel): - channel_id = channel.id - self._private_channels[channel_id] = channel - - if len(self._private_channels) > 128: - _, to_remove = self._private_channels.popitem(last=False) - if isinstance(to_remove, DMChannel): - self._private_channels_by_user.pop(to_remove.recipient.id, None) - - if isinstance(channel, DMChannel): - self._private_channels_by_user[channel.recipient.id] = channel - - def add_dm_channel(self, data): - channel = DMChannel(me=self.user, state=self, data=data) - self._add_private_channel(channel) - return channel - - def _remove_private_channel(self, channel): - self._private_channels.pop(channel.id, None) - if isinstance(channel, DMChannel): - self._private_channels_by_user.pop(channel.recipient.id, None) - - def _get_message(self, msg_id): - return utils.find(lambda m: m.id == msg_id, self._messages) - - def _add_guild_from_data(self, guild): - guild = Guild(data=guild, state=self) - self._add_guild(guild) - return guild - - def chunks_needed(self, guild): - for chunk in range(math.ceil(guild._member_count / 1000)): - yield self.receive_chunk(guild.id) - - @asyncio.coroutine - def request_offline_members(self, guilds): - # get all the chunks - chunks = [] - for guild in guilds: - chunks.extend(self.chunks_needed(guild)) - - # we only want to request ~75 guilds per chunk request. - splits = [guilds[i:i + 75] for i in range(0, len(guilds), 75)] - for split in splits: - yield from self.chunker(split) - - # wait for the chunks - if chunks: - try: - yield from utils.sane_wait_for(chunks, timeout=len(chunks) * 30.0, loop=self.loop) - except asyncio.TimeoutError: - log.info('Somehow timed out waiting for chunks.') - - @asyncio.coroutine - def _delay_ready(self): - try: - launch = self._ready_state.launch - - # only real bots wait for GUILD_CREATE streaming - if self.is_bot: - while not launch.is_set(): - # this snippet of code is basically waiting 2 seconds - # until the last GUILD_CREATE was sent - launch.set() - yield from asyncio.sleep(2, loop=self.loop) - - guilds = self._ready_state.guilds - if self._fetch_offline: - yield from self.request_offline_members(guilds) - - # remove the state - try: - del self._ready_state - except AttributeError: - pass # already been deleted somehow - - # call GUILD_SYNC after we're done chunking - if not self.is_bot: - log.info('Requesting GUILD_SYNC for %s guilds', len(self.guilds)) - yield from self.syncer([s.id for s in self.guilds]) - except asyncio.CancelledError: - pass - else: - # dispatch the event - self.dispatch('ready') - finally: - self._ready_task = None - - def parse_ready(self, data): - if self._ready_task is not None: - self._ready_task.cancel() - - self._ready_state = ReadyState(launch=asyncio.Event(), guilds=[]) - self.clear() - self.user = ClientUser(state=self, data=data['user']) - - guilds = self._ready_state.guilds - for guild_data in data['guilds']: - guild = self._add_guild_from_data(guild_data) - if (not self.is_bot and not guild.unavailable) or guild.large: - guilds.append(guild) - - for relationship in data.get('relationships', []): - try: - r_id = int(relationship['id']) - except KeyError: - continue - else: - self.user._relationships[r_id] = Relationship(state=self, data=relationship) - - for pm in data.get('private_channels', []): - factory, _ = _channel_factory(pm['type']) - self._add_private_channel(factory(me=self.user, data=pm, state=self)) - - self.dispatch('connect') - self._ready_task = compat.create_task(self._delay_ready(), loop=self.loop) - - def parse_resumed(self, data): - self.dispatch('resumed') - - def parse_message_create(self, data): - channel = self.get_channel(int(data['channel_id'])) - message = Message(channel=channel, data=data, state=self) - self.dispatch('message', message) - self._messages.append(message) - - def parse_message_delete(self, data): - message_id = int(data['id']) - channel_id = int(data['channel_id']) - self.dispatch('raw_message_delete', message_id, channel_id) - - found = self._get_message(message_id) - if found is not None: - self.dispatch('message_delete', found) - self._messages.remove(found) - - def parse_message_delete_bulk(self, data): - message_ids = { int(x) for x in data.get('ids', []) } - channel_id = int(data['channel_id']) - self.dispatch('raw_bulk_message_delete', message_ids, channel_id) - to_be_deleted = [message for message in self._messages if message.id in message_ids] - for msg in to_be_deleted: - self.dispatch('message_delete', msg) - self._messages.remove(msg) - - def parse_message_update(self, data): - message_id = int(data['id']) - self.dispatch('raw_message_edit', message_id, data) - message = self._get_message(message_id) - if message is not None: - older_message = copy.copy(message) - if 'call' in data: - # call state message edit - message._handle_call(data['call']) - elif 'content' not in data: - # embed only edit - message.embeds = [Embed.from_data(d) for d in data['embeds']] - else: - message._update(channel=message.channel, data=data) - - self.dispatch('message_edit', older_message, message) - - def parse_message_reaction_add(self, data): - message_id = int(data['message_id']) - user_id = int(data['user_id']) - channel_id = int(data['channel_id']) - - emoji_data = data['emoji'] - emoji_id = utils._get_as_snowflake(emoji_data, 'id') - emoji = PartialReactionEmoji(id=emoji_id, name=emoji_data['name']) - self.dispatch('raw_reaction_add', emoji, message_id, channel_id, user_id) - - # rich interface here - message = self._get_message(message_id) - if message is not None: - emoji = self._upgrade_partial_emoji(emoji) - reaction = message._add_reaction(data, emoji, user_id) - user = self._get_reaction_user(message.channel, user_id) - if user: - self.dispatch('reaction_add', reaction, user) - - def parse_message_reaction_remove_all(self, data): - message_id = int(data['message_id']) - channel_id = int(data['channel_id']) - self.dispatch('raw_reaction_clear', message_id, channel_id) - - message = self._get_message(message_id) - if message is not None: - old_reactions = message.reactions.copy() - message.reactions.clear() - self.dispatch('reaction_clear', message, old_reactions) - - def parse_message_reaction_remove(self, data): - message_id = int(data['message_id']) - user_id = int(data['user_id']) - channel_id = int(data['channel_id']) - - emoji_data = data['emoji'] - emoji_id = utils._get_as_snowflake(emoji_data, 'id') - emoji = PartialReactionEmoji(id=emoji_id, name=emoji_data['name']) - self.dispatch('raw_reaction_remove', emoji, message_id, channel_id, user_id) - - message = self._get_message(message_id) - if message is not None: - emoji = self._upgrade_partial_emoji(emoji) - try: - reaction = message._remove_reaction(data, emoji, user_id) - except (AttributeError, ValueError) as e: # eventual consistency lol - pass - else: - user = self._get_reaction_user(message.channel, user_id) - if user: - self.dispatch('reaction_remove', reaction, user) - - def parse_presence_update(self, data): - guild_id = utils._get_as_snowflake(data, 'guild_id') - guild = self._get_guild(guild_id) - if guild is None: - log.warning('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', guild_id) - return - - user = data['user'] - member_id = int(user['id']) - member = guild.get_member(member_id) - if member is None: - if 'username' not in user: - # sometimes we receive 'incomplete' member data post-removal. - # skip these useless cases. - return - - member = Member(guild=guild, data=data, state=self) - guild._add_member(member) - - old_member = member._copy() - member._presence_update(data=data, user=user) - self.dispatch('member_update', old_member, member) - - def parse_user_update(self, data): - self.user = ClientUser(state=self, data=data) - - def parse_channel_delete(self, data): - guild = self._get_guild(utils._get_as_snowflake(data, 'guild_id')) - channel_id = int(data['id']) - if guild is not None: - channel = guild.get_channel(channel_id) - if channel is not None: - guild._remove_channel(channel) - self.dispatch('guild_channel_delete', channel) - else: - # the reason we're doing this is so it's also removed from the - # private channel by user cache as well - channel = self._get_private_channel(channel_id) - if channel is not None: - self._remove_private_channel(channel) - self.dispatch('private_channel_delete', channel) - - def parse_channel_update(self, data): - channel_type = try_enum(ChannelType, data.get('type')) - channel_id = int(data['id']) - if channel_type is ChannelType.group: - channel = self._get_private_channel(channel_id) - old_channel = copy.copy(channel) - channel._update_group(data) - self.dispatch('private_channel_update', old_channel, channel) - return - - guild_id = utils._get_as_snowflake(data, 'guild_id') - guild = self._get_guild(guild_id) - if guild is not None: - channel = guild.get_channel(channel_id) - if channel is not None: - old_channel = copy.copy(channel) - channel._update(guild, data) - self.dispatch('guild_channel_update', old_channel, channel) - else: - log.warning('CHANNEL_UPDATE referencing an unknown channel ID: %s. Discarding.', channel_id) - else: - log.warning('CHANNEL_UPDATE referencing an unknown guild ID: %s. Discarding.', guild_id) - - def parse_channel_create(self, data): - factory, ch_type = _channel_factory(data['type']) - if factory is None: - log.warning('CHANNEL_CREATE referencing an unknown channel type %s. Discarding.', data['type']) - return - - channel = None - - if ch_type in (ChannelType.group, ChannelType.private): - channel_id = int(data['id']) - if self._get_private_channel(channel_id) is None: - channel = factory(me=self.user, data=data, state=self) - self._add_private_channel(channel) - self.dispatch('private_channel_create', channel) - else: - guild_id = utils._get_as_snowflake(data, 'guild_id') - guild = self._get_guild(guild_id) - if guild is not None: - channel = factory(guild=guild, state=self, data=data) - guild._add_channel(channel) - self.dispatch('guild_channel_create', channel) - else: - log.warning('CHANNEL_CREATE referencing an unknown guild ID: %s. Discarding.', guild_id) - return - - - def parse_channel_pins_update(self, data): - channel_id = int(data['channel_id']) - channel = self.get_channel(channel_id) - if channel is None: - log.warning('CHANNEL_PINS_UPDATE referencing an unknown channel ID: %s. Discarding.', channel_id) - return - - last_pin = utils.parse_time(data['last_pin_timestamp']) if data['last_pin_timestamp'] else None - - try: - # I have not imported discord.abc in this file - # the isinstance check is also 2x slower than just checking this attribute - # so we're just gonna check it since it's easier and faster and lazier - channel.guild - except AttributeError: - self.dispatch('private_channel_pins_update', channel, last_pin) - else: - self.dispatch('guild_channel_pins_update', channel, last_pin) - - def parse_channel_recipient_add(self, data): - channel = self._get_private_channel(int(data['channel_id'])) - user = self.store_user(data['user']) - channel.recipients.append(user) - self.dispatch('group_join', channel, user) - - def parse_channel_recipient_remove(self, data): - channel = self._get_private_channel(int(data['channel_id'])) - user = self.store_user(data['user']) - try: - channel.recipients.remove(user) - except ValueError: - pass - else: - self.dispatch('group_remove', channel, user) - - def parse_guild_member_add(self, data): - guild = self._get_guild(int(data['guild_id'])) - if guild is None: - log.warning('GUILD_MEMBER_ADD referencing an unknown guild ID: %s. Discarding.', data['guild_id']) - return - - member = Member(guild=guild, data=data, state=self) - guild._add_member(member) - guild._member_count += 1 - self.dispatch('member_join', member) - - def parse_guild_member_remove(self, data): - guild = self._get_guild(int(data['guild_id'])) - if guild is not None: - user_id = int(data['user']['id']) - member = guild.get_member(user_id) - if member is not None: - guild._remove_member(member) - guild._member_count -= 1 - self.dispatch('member_remove', member) - else: - log.warning('GUILD_MEMBER_REMOVE referencing an unknown guild ID: %s. Discarding.', data['guild_id']) - - def parse_guild_member_update(self, data): - guild = self._get_guild(int(data['guild_id'])) - user = data['user'] - user_id = int(user['id']) - if guild is None: - log.warning('GUILD_MEMBER_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id']) - return - - member = guild.get_member(user_id) - if member is not None: - old_member = copy.copy(member) - member._update(data, user) - self.dispatch('member_update', old_member, member) - else: - log.warning('GUILD_MEMBER_UPDATE referencing an unknown member ID: %s. Discarding.', user_id) - - def parse_guild_emojis_update(self, data): - guild = self._get_guild(int(data['guild_id'])) - if guild is None: - log.warning('GUILD_EMOJIS_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id']) - return - - before_emojis = guild.emojis - guild.emojis = tuple(map(lambda d: self.store_emoji(guild, d), data['emojis'])) - self.dispatch('guild_emojis_update', guild, before_emojis, guild.emojis) - - def _get_create_guild(self, data): - if data.get('unavailable') == False: - # GUILD_CREATE with unavailable in the response - # usually means that the guild has become available - # and is therefore in the cache - guild = self._get_guild(int(data['id'])) - if guild is not None: - guild.unavailable = False - guild._from_data(data) - return guild - - return self._add_guild_from_data(data) - - @asyncio.coroutine - def _chunk_and_dispatch(self, guild, unavailable): - chunks = list(self.chunks_needed(guild)) - yield from self.chunker(guild) - if chunks: - try: - yield from utils.sane_wait_for(chunks, timeout=len(chunks), loop=self.loop) - except asyncio.TimeoutError: - log.info('Somehow timed out waiting for chunks.') - - if unavailable == False: - self.dispatch('guild_available', guild) - else: - self.dispatch('guild_join', guild) - - def parse_guild_create(self, data): - unavailable = data.get('unavailable') - if unavailable == True: - # joined a guild with unavailable == True so.. - return - - guild = self._get_create_guild(data) - - # check if it requires chunking - if guild.large: - if unavailable == False: - # check if we're waiting for 'useful' READY - # and if we are, we don't want to dispatch any - # event such as guild_join or guild_available - # because we're still in the 'READY' phase. Or - # so we say. - try: - state = self._ready_state - state.launch.clear() - state.guilds.append(guild) - except AttributeError: - # the _ready_state attribute is only there during - # processing of useful READY. - pass - else: - return - - # since we're not waiting for 'useful' READY we'll just - # do the chunk request here if wanted - if self._fetch_offline: - compat.create_task(self._chunk_and_dispatch(guild, unavailable), loop=self.loop) - return - - # Dispatch available if newly available - if unavailable == False: - self.dispatch('guild_available', guild) - else: - self.dispatch('guild_join', guild) - - def parse_guild_sync(self, data): - guild = self._get_guild(int(data['id'])) - guild._sync(data) - - def parse_guild_update(self, data): - guild = self._get_guild(int(data['id'])) - if guild is not None: - old_guild = copy.copy(guild) - guild._from_data(data) - self.dispatch('guild_update', old_guild, guild) - else: - log.warning('GUILD_UPDATE referencing an unknown guild ID: %s. Discarding.', data['id']) - - def parse_guild_delete(self, data): - guild = self._get_guild(int(data['id'])) - if guild is None: - log.warning('GUILD_DELETE referencing an unknown guild ID: %s. Discarding.', data['id']) - return - - if data.get('unavailable', False) and guild is not None: - # GUILD_DELETE with unavailable being True means that the - # guild that was available is now currently unavailable - guild.unavailable = True - self.dispatch('guild_unavailable', guild) - return - - # do a cleanup of the messages cache - self._messages = deque((msg for msg in self._messages if msg.guild != guild), maxlen=self.max_messages) - - self._remove_guild(guild) - self.dispatch('guild_remove', guild) - - def parse_guild_ban_add(self, data): - # we make the assumption that GUILD_BAN_ADD is done - # before GUILD_MEMBER_REMOVE is called - # hence we don't remove it from cache or do anything - # strange with it, the main purpose of this event - # is mainly to dispatch to another event worth listening to for logging - guild = self._get_guild(int(data['guild_id'])) - if guild is not None: - try: - user = User(data=data['user'], state=self) - except KeyError: - pass - else: - member = guild.get_member(user.id) or user - self.dispatch('member_ban', guild, member) - - def parse_guild_ban_remove(self, data): - guild = self._get_guild(int(data['guild_id'])) - if guild is not None: - if 'user' in data: - user = self.store_user(data['user']) - self.dispatch('member_unban', guild, user) - - def parse_guild_role_create(self, data): - guild = self._get_guild(int(data['guild_id'])) - if guild is None: - log.warning('GUILD_ROLE_CREATE referencing an unknown guild ID: %s. Discarding.', data['guild_id']) - return - - role_data = data['role'] - role = Role(guild=guild, data=role_data, state=self) - guild._add_role(role) - self.dispatch('guild_role_create', role) - - def parse_guild_role_delete(self, data): - guild = self._get_guild(int(data['guild_id'])) - if guild is not None: - role_id = int(data['role_id']) - role = utils.find(lambda r: r.id == role_id, guild.roles) - try: - guild._remove_role(role) - except ValueError: - return - else: - self.dispatch('guild_role_delete', role) - else: - log.warning('GUILD_ROLE_DELETE referencing an unknown guild ID: %s. Discarding.', data['guild_id']) - - def parse_guild_role_update(self, data): - guild = self._get_guild(int(data['guild_id'])) - if guild is not None: - role_data = data['role'] - role_id = int(role_data['id']) - role = utils.find(lambda r: r.id == role_id, guild.roles) - if role is not None: - old_role = copy.copy(role) - role._update(role_data) - self.dispatch('guild_role_update', old_role, role) - else: - log.warning('GUILD_ROLE_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id']) - - def parse_guild_members_chunk(self, data): - guild_id = int(data['guild_id']) - guild = self._get_guild(guild_id) - members = data.get('members', []) - for member in members: - m = Member(guild=guild, data=member, state=self) - existing = guild.get_member(m.id) - if existing is None or existing.joined_at is None: - guild._add_member(m) - - log.info('Processed a chunk for %s members in guild ID %s.', len(members), guild_id) - self.process_listeners(ListenerType.chunk, guild, len(members)) - - def parse_voice_state_update(self, data): - guild = self._get_guild(utils._get_as_snowflake(data, 'guild_id')) - channel_id = utils._get_as_snowflake(data, 'channel_id') - if guild is not None: - if int(data['user_id']) == self.user.id: - voice = self._get_voice_client(guild.id) - if voice is not None: - ch = guild.get_channel(channel_id) - if ch is not None: - voice.channel = ch - - member, before, after = guild._update_voice_state(data, channel_id) - if member is not None: - self.dispatch('voice_state_update', member, before, after) - else: - log.warning('VOICE_STATE_UPDATE referencing an unknown member ID: %s. Discarding.', data['user_id']) - else: - # in here we're either at private or group calls - call = self._calls.get(channel_id) - if call is not None: - call._update_voice_state(data) - - def parse_voice_server_update(self, data): - try: - key_id = int(data['guild_id']) - except KeyError: - key_id = int(data['channel_id']) - - vc = self._get_voice_client(key_id) - if vc is not None: - compat.create_task(vc._create_socket(key_id, data)) - - def parse_typing_start(self, data): - channel = self.get_channel(int(data['channel_id'])) - if channel is not None: - member = None - user_id = utils._get_as_snowflake(data, 'user_id') - if isinstance(channel, DMChannel): - member = channel.recipient - elif isinstance(channel, TextChannel): - member = channel.guild.get_member(user_id) - elif isinstance(channel, GroupChannel): - member = utils.find(lambda x: x.id == user_id, channel.recipients) - - if member is not None: - timestamp = datetime.datetime.utcfromtimestamp(data.get('timestamp')) - self.dispatch('typing', channel, member, timestamp) - - def parse_relationship_add(self, data): - key = int(data['id']) - old = self.user.get_relationship(key) - new = Relationship(state=self, data=data) - self.user._relationships[key] = new - if old is not None: - self.dispatch('relationship_update', old, new) - else: - self.dispatch('relationship_add', new) - - def parse_relationship_remove(self, data): - key = int(data['id']) - try: - old = self.user._relationships.pop(key) - except KeyError: - pass - else: - self.dispatch('relationship_remove', old) - - def _get_reaction_user(self, channel, user_id): - if isinstance(channel, TextChannel): - return channel.guild.get_member(user_id) - return self.get_user(user_id) - - def get_reaction_emoji(self, data): - emoji_id = utils._get_as_snowflake(data, 'id') - - if not emoji_id: - return data['name'] - - try: - return self._emojis[emoji_id] - except KeyError: - return PartialReactionEmoji(id=emoji_id, name=data['name']) - - def _upgrade_partial_emoji(self, emoji): - emoji_id = emoji.id - if not emoji_id: - return emoji.name - try: - return self._emojis[emoji_id] - except KeyError: - return emoji - - def get_channel(self, id): - if id is None: - return None - - pm = self._get_private_channel(id) - if pm is not None: - return pm - - for guild in self.guilds: - channel = guild.get_channel(id) - if channel is not None: - return channel - - def create_message(self, *, channel, data): - return Message(state=self, channel=channel, data=data) - - def receive_chunk(self, guild_id): - future = compat.create_future(self.loop) - listener = Listener(ListenerType.chunk, future, lambda s: s.id == guild_id) - self._listeners.append(listener) - return future - -class AutoShardedConnectionState(ConnectionState): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._ready_task = None - - @asyncio.coroutine - def request_offline_members(self, guilds, *, shard_id): - # get all the chunks - chunks = [] - for guild in guilds: - chunks.extend(self.chunks_needed(guild)) - - # we only want to request ~75 guilds per chunk request. - splits = [guilds[i:i + 75] for i in range(0, len(guilds), 75)] - for split in splits: - yield from self.chunker(split, shard_id=shard_id) - - # wait for the chunks - if chunks: - try: - yield from utils.sane_wait_for(chunks, timeout=len(chunks) * 30.0, loop=self.loop) - except asyncio.TimeoutError: - log.info('Somehow timed out waiting for chunks.') - - @asyncio.coroutine - def _delay_ready(self): - launch = self._ready_state.launch - while not launch.is_set(): - # this snippet of code is basically waiting 2 seconds - # until the last GUILD_CREATE was sent - launch.set() - yield from asyncio.sleep(2.0 * self.shard_count, loop=self.loop) - - if self._fetch_offline: - guilds = sorted(self._ready_state.guilds, key=lambda g: g.shard_id) - - for shard_id, sub_guilds in itertools.groupby(guilds, key=lambda g: g.shard_id): - sub_guilds = list(sub_guilds) - yield from self.request_offline_members(sub_guilds, shard_id=shard_id) - self.dispatch('shard_ready', shard_id) - - # remove the state - try: - del self._ready_state - except AttributeError: - pass # already been deleted somehow - - # regular users cannot shard so we won't worry about it here. - - # clear the current task - self._ready_task = None - - # dispatch the event - self.dispatch('ready') - - def parse_ready(self, data): - if not hasattr(self, '_ready_state'): - self._ready_state = ReadyState(launch=asyncio.Event(), guilds=[]) - - self.user = ClientUser(state=self, data=data['user']) - - guilds = self._ready_state.guilds - for guild_data in data['guilds']: - guild = self._add_guild_from_data(guild_data) - if guild.large: - guilds.append(guild) - - for pm in data.get('private_channels', []): - factory, _ = _channel_factory(pm['type']) - self._add_private_channel(factory(me=self.user, data=pm, state=self)) - - self.dispatch('connect') - if self._ready_task is None: - self._ready_task = compat.create_task(self._delay_ready(), loop=self.loop) diff --git a/discord.py-rewrite/discord/user.py b/discord.py-rewrite/discord/user.py deleted file mode 100644 index 7bc5c2e..0000000 --- a/discord.py-rewrite/discord/user.py +++ /dev/null @@ -1,617 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from .utils import snowflake_time, _bytes_to_base64_data, parse_time, valid_icon_size -from .enums import DefaultAvatar, RelationshipType, UserFlags -from .errors import ClientException, InvalidArgument - -from collections import namedtuple - -import discord.abc -import asyncio - -VALID_STATIC_FORMATS = {"jpeg", "jpg", "webp", "png"} -VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"} - -class Profile(namedtuple('Profile', 'flags user mutual_guilds connected_accounts premium_since')): - __slots__ = () - - @property - def nitro(self): - return self.premium_since is not None - - premium = nitro - - def _has_flag(self, o): - v = o.value - return (self.flags & v) == v - - @property - def staff(self): - return self._has_flag(UserFlags.staff) - - @property - def hypesquad(self): - return self._has_flag(UserFlags.hypesquad) - - @property - def partner(self): - return self._has_flag(UserFlags.partner) - - -_BaseUser = discord.abc.User - -class BaseUser(_BaseUser): - __slots__ = ('name', 'id', 'discriminator', 'avatar', 'bot', '_state') - - def __init__(self, *, state, data): - self._state = state - self.name = data['username'] - self.id = int(data['id']) - self.discriminator = data['discriminator'] - self.avatar = data['avatar'] - self.bot = data.get('bot', False) - - def __str__(self): - return '{0.name}#{0.discriminator}'.format(self) - - def __eq__(self, other): - return isinstance(other, _BaseUser) and other.id == self.id - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return self.id >> 22 - - @property - def avatar_url(self): - """Returns a friendly URL version of the avatar the user has. - - If the user does not have a traditional avatar, their default - avatar URL is returned instead. - - This is equivalent to calling :meth:`avatar_url_as` with - the default parameters (i.e. webp/gif detection and a size of 1024). - """ - return self.avatar_url_as(format=None, size=1024) - - def is_avatar_animated(self): - """bool: Returns True if the user has an animated avatar.""" - return self.avatar and self.avatar.startswith('a_') - - def avatar_url_as(self, *, format=None, static_format='webp', size=1024): - """Returns a friendly URL version of the avatar the user has. - - If the user does not have a traditional avatar, their default - avatar URL is returned instead. - - The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif', and - 'gif' is only valid for animated avatars. The size must be a power of 2 - between 16 and 1024. - - Parameters - ----------- - format: Optional[str] - The format to attempt to convert the avatar to. - If the format is ``None``, then it is automatically - detected into either 'gif' or static_format depending on the - avatar being animated or not. - static_format: 'str' - Format to attempt to convert only non-animated avatars to. - Defaults to 'webp' - size: int - The size of the image to display. - - Returns - -------- - str - The resulting CDN URL. - - Raises - ------ - InvalidArgument - Bad image format passed to ``format`` or ``static_format``, or - invalid ``size``. - """ - if not valid_icon_size(size): - raise InvalidArgument("size must be a power of 2 between 16 and 1024") - if format is not None and format not in VALID_AVATAR_FORMATS: - raise InvalidArgument("format must be None or one of {}".format(VALID_AVATAR_FORMATS)) - if format == "gif" and not self.is_avatar_animated(): - raise InvalidArgument("non animated avatars do not support gif format") - if static_format not in VALID_STATIC_FORMATS: - raise InvalidArgument("static_format must be one of {}".format(VALID_STATIC_FORMATS)) - - if self.avatar is None: - return self.default_avatar_url - - if format is None: - if self.is_avatar_animated(): - format = 'gif' - else: - format = static_format - - return 'https://cdn.discordapp.com/avatars/{0.id}/{0.avatar}.{1}?size={2}'.format(self, format, size) - - @property - def default_avatar(self): - """Returns the default avatar for a given user. This is calculated by the user's descriminator""" - return DefaultAvatar(int(self.discriminator) % len(DefaultAvatar)) - - @property - def default_avatar_url(self): - """Returns a URL for a user's default avatar.""" - return 'https://cdn.discordapp.com/embed/avatars/{}.png'.format(self.default_avatar.value) - - @property - def mention(self): - """Returns a string that allows you to mention the given user.""" - return '<@{0.id}>'.format(self) - - def permissions_in(self, channel): - """An alias for :meth:`abc.GuildChannel.permissions_for`. - - Basically equivalent to: - - .. code-block:: python3 - - channel.permissions_for(self) - - Parameters - ----------- - channel - The channel to check your permissions for. - """ - return channel.permissions_for(self) - - @property - def created_at(self): - """Returns the user's creation time in UTC. - - This is when the user's discord account was created.""" - return snowflake_time(self.id) - - @property - def display_name(self): - """Returns the user's display name. - - For regular users this is just their username, but - if they have a guild specific nickname then that - is returned instead. - """ - return self.name - - def mentioned_in(self, message): - """Checks if the user is mentioned in the specified message. - - Parameters - ----------- - message : :class:`Message` - The message to check if you're mentioned in. - """ - - if message.mention_everyone: - return True - - for user in message.mentions: - if user.id == self.id: - return True - - return False - -class ClientUser(BaseUser): - """Represents your Discord user. - - .. container:: operations - - .. describe:: x == y - - Checks if two users are equal. - - .. describe:: x != y - - Checks if two users are not equal. - - .. describe:: hash(x) - - Return the user's hash. - - .. describe:: str(x) - - Returns the user's name with discriminator. - - Attributes - ----------- - name: str - The user's username. - id: int - The user's unique ID. - discriminator: str - The user's discriminator. This is given when the username has conflicts. - avatar: Optional[str] - The avatar hash the user has. Could be None. - bot: bool - Specifies if the user is a bot account. - verified: bool - Specifies if the user is a verified account. - email: Optional[str] - The email the user used when registering. - mfa_enabled: bool - Specifies if the user has MFA turned on and working. - premium: bool - Specifies if the user is a premium user (e.g. has Discord Nitro). - """ - __slots__ = ('email', 'verified', 'mfa_enabled', 'premium', '_relationships') - - def __init__(self, *, state, data): - super().__init__(state=state, data=data) - self.verified = data.get('verified', False) - self.email = data.get('email') - self.mfa_enabled = data.get('mfa_enabled', False) - self.premium = data.get('premium', False) - self._relationships = {} - - def __repr__(self): - return ''.format(self) - - - def get_relationship(self, user_id): - """Retrieves the :class:`Relationship` if applicable. - - Parameters - ----------- - user_id: int - The user ID to check if we have a relationship with them. - - Returns - -------- - Optional[:class:`Relationship`] - The relationship if available or ``None`` - """ - return self._relationships.get(user_id) - - @property - def relationships(self): - """Returns a list of :class:`Relationship` that the user has.""" - return list(self._relationships.values()) - - @property - def friends(self): - """Returns a list of :class:`User`\s that the user is friends with.""" - return [r.user for r in self._relationships.values() if r.type is RelationshipType.friend] - - @property - def blocked(self): - """Returns a list of :class:`User`\s that the user has blocked.""" - return [r.user for r in self._relationships.values() if r.type is RelationshipType.blocked] - - @asyncio.coroutine - def edit(self, **fields): - """|coro| - - Edits the current profile of the client. - - If a bot account is used then a password field is optional, - otherwise it is required. - - Note - ----- - To upload an avatar, a *bytes-like object* must be passed in that - represents the image being uploaded. If this is done through a file - then the file must be opened via ``open('some_filename', 'rb')`` and - the *bytes-like object* is given through the use of ``fp.read()``. - - The only image formats supported for uploading is JPEG and PNG. - - Parameters - ----------- - password : str - The current password for the client's account. - Only applicable to user accounts. - new_password: str - The new password you wish to change to. - Only applicable to user accounts. - email: str - The new email you wish to change to. - Only applicable to user accounts. - username :str - The new username you wish to change to. - avatar: bytes - A *bytes-like object* representing the image to upload. - Could be ``None`` to denote no avatar. - - Raises - ------ - HTTPException - Editing your profile failed. - InvalidArgument - Wrong image format passed for ``avatar``. - ClientException - Password is required for non-bot accounts. - """ - - try: - avatar_bytes = fields['avatar'] - except KeyError: - avatar = self.avatar - else: - if avatar_bytes is not None: - avatar = _bytes_to_base64_data(avatar_bytes) - else: - avatar = None - - not_bot_account = not self.bot - password = fields.get('password') - if not_bot_account and password is None: - raise ClientException('Password is required for non-bot accounts.') - - args = { - 'password': password, - 'username': fields.get('username', self.name), - 'avatar': avatar - } - - if not_bot_account: - args['email'] = fields.get('email', self.email) - - if 'new_password' in fields: - args['new_password'] = fields['new_password'] - - http = self._state.http - - data = yield from http.edit_profile(**args) - if not_bot_account: - self.email = data['email'] - try: - http._token(data['token'], bot=False) - except KeyError: - pass - - # manually update data by calling __init__ explicitly. - self.__init__(state=self._state, data=data) - - @asyncio.coroutine - def create_group(self, *recipients): - """|coro| - - Creates a group direct message with the recipients - provided. These recipients must be have a relationship - of type :attr:`RelationshipType.friend`. - - Bot accounts cannot create a group. - - Parameters - ----------- - \*recipients - An argument list of :class:`User` to have in - your group. - - Return - ------- - :class:`GroupChannel` - The new group channel. - - Raises - ------- - HTTPException - Failed to create the group direct message. - ClientException - Attempted to create a group with only one recipient. - This does not include yourself. - """ - - from .channel import GroupChannel - - if len(recipients) < 2: - raise ClientException('You must have two or more recipients to create a group.') - - users = [str(u.id) for u in recipients] - data = yield from self._state.http.create_group(self.id, users) - return GroupChannel(me=self, data=data, state=self._state) - -class User(BaseUser, discord.abc.Messageable): - """Represents a Discord user. - - .. container:: operations - - .. describe:: x == y - - Checks if two users are equal. - - .. describe:: x != y - - Checks if two users are not equal. - - .. describe:: hash(x) - - Return the user's hash. - - .. describe:: str(x) - - Returns the user's name with discriminator. - - Attributes - ----------- - name: str - The user's username. - id: int - The user's unique ID. - discriminator: str - The user's discriminator. This is given when the username has conflicts. - avatar: Optional[str] - The avatar hash the user has. Could be None. - bot: bool - Specifies if the user is a bot account. - """ - - __slots__ = ('__weakref__') - - def __repr__(self): - return ''.format(self) - - @asyncio.coroutine - def _get_channel(self): - ch = yield from self.create_dm() - return ch - - @property - def dm_channel(self): - """Returns the :class:`DMChannel` associated with this user if it exists. - - If this returns ``None``, you can create a DM channel by calling the - :meth:`create_dm` coroutine function. - """ - return self._state._get_private_channel_by_user(self.id) - - @asyncio.coroutine - def create_dm(self): - """Creates a :class:`DMChannel` with this user. - - This should be rarely called, as this is done transparently for most - people. - """ - found = self.dm_channel - if found is not None: - return found - - state = self._state - data = yield from state.http.start_private_message(self.id) - return state.add_dm_channel(data) - - @property - def relationship(self): - """Returns the :class:`Relationship` with this user if applicable, ``None`` otherwise.""" - return self._state.user.get_relationship(self.id) - - def is_friend(self): - """bool: Checks if the user is your friend.""" - r = self.relationship - if r is None: - return False - return r.type is RelationshipType.friend - - def is_blocked(self): - """bool: Checks if the user is blocked.""" - r = self.relationship - if r is None: - return False - return r.type is RelationshipType.blocked - - @asyncio.coroutine - def block(self): - """|coro| - - Blocks the user. - - Raises - ------- - Forbidden - Not allowed to block this user. - HTTPException - Blocking the user failed. - """ - - yield from self._state.http.add_relationship(self.id, type=RelationshipType.blocked.value) - - @asyncio.coroutine - def unblock(self): - """|coro| - - Unblocks the user. - - Raises - ------- - Forbidden - Not allowed to unblock this user. - HTTPException - Unblocking the user failed. - """ - yield from self._state.http.remove_relationship(self.id) - - @asyncio.coroutine - def remove_friend(self): - """|coro| - - Removes the user as a friend. - - Raises - ------- - Forbidden - Not allowed to remove this user as a friend. - HTTPException - Removing the user as a friend failed. - """ - yield from self._state.http.remove_relationship(self.id) - - @asyncio.coroutine - def send_friend_request(self): - """|coro| - - Sends the user a friend request. - - Raises - ------- - Forbidden - Not allowed to send a friend request to the user. - HTTPException - Sending the friend request failed. - """ - yield from self._state.http.send_friend_request(username=self.name, discriminator=self.discriminator) - - @asyncio.coroutine - def profile(self): - """|coro| - - Gets the user's profile. This can only be used by non-bot accounts. - - Raises - ------- - Forbidden - Not allowed to fetch profiles. - HTTPException - Fetching the profile failed. - - Returns - -------- - :class:`Profile` - The profile of the user. - """ - - state = self._state - data = yield from state.http.get_user_profile(self.id) - - def transform(d): - return state._get_guild(int(d['id'])) - - since = data.get('premium_since') - mutual_guilds = list(filter(None, map(transform, data.get('mutual_guilds', [])))) - return Profile(flags=data['user'].get('flags', 0), - premium_since=parse_time(since), - mutual_guilds=mutual_guilds, - user=self, - connected_accounts=data['connected_accounts']) diff --git a/discord.py-rewrite/discord/utils.py b/discord.py-rewrite/discord/utils.py deleted file mode 100644 index 66c235b..0000000 --- a/discord.py-rewrite/discord/utils.py +++ /dev/null @@ -1,294 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from re import split as re_split -from .errors import InvalidArgument -import datetime -from base64 import b64encode -from email.utils import parsedate_to_datetime -import asyncio -import json -import warnings, functools - -DISCORD_EPOCH = 1420070400000 - -class cached_property: - def __init__(self, function): - self.function = function - self.__doc__ = getattr(function, '__doc__') - - def __get__(self, instance, owner): - if instance is None: - return self - - value = self.function(instance) - setattr(instance, self.function.__name__, value) - - return value - -class CachedSlotProperty: - def __init__(self, name, function): - self.name = name - self.function = function - self.__doc__ = getattr(function, '__doc__') - - def __get__(self, instance, owner): - if instance is None: - return self - - try: - return getattr(instance, self.name) - except AttributeError: - value = self.function(instance) - setattr(instance, self.name, value) - return value - -def cached_slot_property(name): - def decorator(func): - return CachedSlotProperty(name, func) - return decorator - -def parse_time(timestamp): - if timestamp: - return datetime.datetime(*map(int, re_split(r'[^\d]', timestamp.replace('+00:00', '')))) - return None - -def deprecated(instead=None): - def actual_decorator(func): - @functools.wraps(func) - def decorated(*args, **kwargs): - warnings.simplefilter('always', DeprecationWarning) # turn off filter - if instead: - fmt = "{0.__name__} is deprecated, use {1} instead." - else: - fmt = '{0.__name__} is deprecated.' - - warnings.warn(fmt.format(func, instead), stacklevel=3, category=DeprecationWarning) - warnings.simplefilter('default', DeprecationWarning) # reset filter - return func(*args, **kwargs) - return decorated - return actual_decorator - -def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None): - """A helper function that returns the OAuth2 URL for inviting the bot - into guilds. - - Parameters - ----------- - client_id : str - The client ID for your bot. - permissions : :class:`Permissions` - The permissions you're requesting. If not given then you won't be requesting any - permissions. - guild : :class:`Guild` - The guild to pre-select in the authorization screen, if available. - redirect_uri : str - An optional valid redirect URI. - """ - url = 'https://discordapp.com/oauth2/authorize?client_id={}&scope=bot'.format(client_id) - if permissions is not None: - url = url + '&permissions=' + str(permissions.value) - if guild is not None: - url = url + "&guild_id=" + guild.id - if redirect_uri is not None: - from urllib.parse import urlencode - url = url + "&response_type=code&" + urlencode({'redirect_uri': redirect_uri}) - return url - - -def snowflake_time(id): - """Returns the creation date in UTC of a discord id.""" - return datetime.datetime.utcfromtimestamp(((id >> 22) + DISCORD_EPOCH) / 1000) - -def time_snowflake(datetime_obj, high=False): - """Returns a numeric snowflake pretending to be created at the given date. - - When using as the lower end of a range, use time_snowflake(high=False) - 1 to be inclusive, high=True to be exclusive - When using as the higher end of a range, use time_snowflake(high=True) + 1 to be inclusive, high=False to be exclusive - - Parameters - ----------- - datetime_obj - A timezone-naive datetime object representing UTC time. - high - Whether or not to set the lower 22 bit to high or low. - """ - unix_seconds = (datetime_obj - type(datetime_obj)(1970, 1, 1)).total_seconds() - discord_millis = int(unix_seconds * 1000 - DISCORD_EPOCH) - - return (discord_millis << 22) + (2**22-1 if high else 0) - -def find(predicate, seq): - """A helper to return the first element found in the sequence - that meets the predicate. For example: :: - - member = find(lambda m: m.name == 'Mighty', channel.guild.members) - - would find the first :class:`Member` whose name is 'Mighty' and return it. - If an entry is not found, then ``None`` is returned. - - This is different from `filter`_ due to the fact it stops the moment it finds - a valid entry. - - - .. _filter: https://docs.python.org/3.6/library/functions.html#filter - - Parameters - ----------- - predicate - A function that returns a boolean-like result. - seq : iterable - The iterable to search through. - """ - - for element in seq: - if predicate(element): - return element - return None - -def get(iterable, **attrs): - """A helper that returns the first element in the iterable that meets - all the traits passed in ``attrs``. This is an alternative for - :func:`discord.utils.find`. - - When multiple attributes are specified, they are checked using - logical AND, not logical OR. Meaning they have to meet every - attribute passed in and not one of them. - - To have a nested attribute search (i.e. search by ``x.y``) then - pass in ``x__y`` as the keyword argument. - - If nothing is found that matches the attributes passed, then - ``None`` is returned. - - Examples - --------- - - Basic usage: - - .. code-block:: python3 - - member = discord.utils.get(message.guild.members, name='Foo') - - Multiple attribute matching: - - .. code-block:: python3 - - channel = discord.utils.get(guild.voice_channels, name='Foo', bitrate=64000) - - Nested attribute matching: - - .. code-block:: python3 - - channel = discord.utils.get(client.get_all_channels(), guild__name='Cool', name='general') - - Parameters - ----------- - iterable - An iterable to search through. - \*\*attrs - Keyword arguments that denote attributes to search with. - """ - - def predicate(elem): - for attr, val in attrs.items(): - nested = attr.split('__') - obj = elem - for attribute in nested: - obj = getattr(obj, attribute) - - if obj != val: - return False - return True - - return find(predicate, iterable) - - -def _unique(iterable): - seen = set() - adder = seen.add - return [x for x in iterable if not (x in seen or adder(x))] - -def _get_as_snowflake(data, key): - try: - value = data[key] - except KeyError: - return None - else: - return value and int(value) - -def _get_mime_type_for_image(data): - if data.startswith(b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'): - return 'image/png' - elif data.startswith(b'\xFF\xD8') and data.rstrip(b'\0').endswith(b'\xFF\xD9'): - return 'image/jpeg' - elif data.startswith(b'\x47\x49\x46\x38\x37\x61') or data.startswith(b'\x47\x49\x46\x38\x39\x61'): - return 'image/gif' - else: - raise InvalidArgument('Unsupported image type given') - -def _bytes_to_base64_data(data): - fmt = 'data:{mime};base64,{data}' - mime = _get_mime_type_for_image(data) - b64 = b64encode(data).decode('ascii') - return fmt.format(mime=mime, data=b64) - -def to_json(obj): - return json.dumps(obj, separators=(',', ':'), ensure_ascii=True) - -def _parse_ratelimit_header(request): - now = parsedate_to_datetime(request.headers['Date']) - reset = datetime.datetime.fromtimestamp(int(request.headers['X-Ratelimit-Reset']), datetime.timezone.utc) - return (reset - now).total_seconds() - -@asyncio.coroutine -def maybe_coroutine(f, *args, **kwargs): - value = f(*args, **kwargs) - if asyncio.iscoroutine(value): - return (yield from value) - else: - return value - -@asyncio.coroutine -def async_all(gen): - check = asyncio.iscoroutine - for elem in gen: - if check(elem): - elem = yield from elem - if not elem: - return False - return True - -@asyncio.coroutine -def sane_wait_for(futures, *, timeout, loop): - done, pending = yield from asyncio.wait(futures, timeout=timeout, loop=loop) - - if len(pending) != 0: - raise asyncio.TimeoutError() - -def valid_icon_size(size): - """Icons must be power of 2 within [16, 1024].""" - return ((size != 0) and not (size & (size - 1))) and size in range(16, 1025) diff --git a/discord.py-rewrite/discord/voice_client.py b/discord.py-rewrite/discord/voice_client.py deleted file mode 100644 index 9fcc4ea..0000000 --- a/discord.py-rewrite/discord/voice_client.py +++ /dev/null @@ -1,426 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -"""Some documentation to refer to: - -- Our main web socket (mWS) sends opcode 4 with a guild ID and channel ID. -- The mWS receives VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE. -- We pull the session_id from VOICE_STATE_UPDATE. -- We pull the token, endpoint and server_id from VOICE_SERVER_UPDATE. -- Then we initiate the voice web socket (vWS) pointing to the endpoint. -- We send opcode 0 with the user_id, server_id, session_id and token using the vWS. -- The vWS sends back opcode 2 with an ssrc, port, modes(array) and hearbeat_interval. -- We send a UDP discovery packet to endpoint:port and receive our IP and our port in LE. -- Then we send our IP and port via vWS with opcode 1. -- When that's all done, we receive opcode 4 from the vWS. -- Finally we can transmit data to endpoint:port. -""" - -import asyncio -import socket -import logging -import struct -import threading - -log = logging.getLogger(__name__) - -try: - import nacl.secret - has_nacl = True -except ImportError: - has_nacl = False - -from . import opus -from .backoff import ExponentialBackoff -from .gateway import * -from .errors import ClientException, ConnectionClosed -from .player import AudioPlayer, AudioSource - -class VoiceClient: - """Represents a Discord voice connection. - - You do not create these, you typically get them from - e.g. :meth:`VoiceChannel.connect`. - - Warning - -------- - In order to play audio, you must have loaded the opus library - through :func:`opus.load_opus`. - - If you don't do this then the library will not be able to - transmit audio. - - Attributes - ----------- - session_id: str - The voice connection session ID. - token: str - The voice connection token. - endpoint: str - The endpoint we are connecting to. - channel: :class:`abc.Connectable` - The voice channel connected to. - loop - The event loop that the voice client is running on. - """ - def __init__(self, state, timeout, channel): - if not has_nacl: - raise RuntimeError("PyNaCl library needed in order to use voice") - - self.channel = channel - self.main_ws = None - self.timeout = timeout - self.ws = None - self.socket = None - self.loop = state.loop - self._state = state - # this will be used in the AudioPlayer thread - self._connected = threading.Event() - self._handshake_complete = asyncio.Event(loop=self.loop) - - self._connections = 0 - self.sequence = 0 - self.timestamp = 0 - self._runner = None - self._player = None - self.encoder = opus.Encoder() - - warn_nacl = not has_nacl - - @property - def guild(self): - """Optional[:class:`Guild`]: The guild we're connected to, if applicable.""" - return getattr(self.channel, 'guild', None) - - @property - def user(self): - """:class:`ClientUser`: The user connected to voice (i.e. ourselves).""" - return self._state.user - - def checked_add(self, attr, value, limit): - val = getattr(self, attr) - if val + value > limit: - setattr(self, attr, 0) - else: - setattr(self, attr, val + value) - - # connection related - - @asyncio.coroutine - def start_handshake(self): - log.info('Starting voice handshake...') - - key_id, key_name = self.channel._get_voice_client_key() - guild_id, channel_id = self.channel._get_voice_state_pair() - state = self._state - self.main_ws = ws = state._get_websocket(guild_id) - self._connections += 1 - - # request joining - yield from ws.voice_state(guild_id, channel_id) - - try: - yield from asyncio.wait_for(self._handshake_complete.wait(), timeout=self.timeout, loop=self.loop) - except asyncio.TimeoutError as e: - yield from self.terminate_handshake(remove=True) - raise e - - log.info('Voice handshake complete. Endpoint found %s (IP: %s)', self.endpoint, self.endpoint_ip) - - @asyncio.coroutine - def terminate_handshake(self, *, remove=False): - guild_id, channel_id = self.channel._get_voice_state_pair() - self._handshake_complete.clear() - yield from self.main_ws.voice_state(guild_id, None, self_mute=True) - - log.info('The voice handshake is being terminated for Channel ID %s (Guild ID %s)', channel_id, guild_id) - if remove: - log.info('The voice client has been removed for Channel ID %s (Guild ID %s)', channel_id, guild_id) - key_id, _ = self.channel._get_voice_client_key() - self._state._remove_voice_client(key_id) - - @asyncio.coroutine - def _create_socket(self, server_id, data): - self._connected.clear() - self.session_id = self.main_ws.session_id - self.server_id = server_id - self.token = data.get('token') - endpoint = data.get('endpoint') - - if endpoint is None or self.token is None: - log.warning('Awaiting endpoint... This requires waiting. ' \ - 'If timeout occurred considering raising the timeout and reconnecting.') - return - - self.endpoint = endpoint.replace(':80', '') - self.endpoint_ip = socket.gethostbyname(self.endpoint) - - if self.socket: - try: - self.socket.close() - except: - pass - - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.socket.setblocking(False) - - if self._handshake_complete.is_set(): - # terminate the websocket and handle the reconnect loop if necessary. - self._handshake_complete.clear() - yield from self.ws.close(1006) - return - - self._handshake_complete.set() - - @asyncio.coroutine - def connect(self, *, reconnect=True, _tries=0, do_handshake=True): - log.info('Connecting to voice...') - try: - del self.secret_key - except AttributeError: - pass - - if do_handshake: - yield from self.start_handshake() - - try: - self.ws = yield from DiscordVoiceWebSocket.from_client(self) - self._connected.clear() - while not hasattr(self, 'secret_key'): - yield from self.ws.poll_event() - self._connected.set() - except (ConnectionClosed, asyncio.TimeoutError): - if reconnect and _tries < 5: - log.exception('Failed to connect to voice... Retrying...') - yield from asyncio.sleep(1 + _tries * 2.0, loop=self.loop) - yield from self.terminate_handshake() - yield from self.connect(reconnect=reconnect, _tries=_tries + 1) - else: - raise - - if self._runner is None: - self._runner = self.loop.create_task(self.poll_voice_ws(reconnect)) - - @asyncio.coroutine - def poll_voice_ws(self, reconnect): - backoff = ExponentialBackoff() - while True: - try: - yield from self.ws.poll_event() - except (ConnectionClosed, asyncio.TimeoutError) as e: - if isinstance(e, ConnectionClosed): - if e.code == 1000: - yield from self.disconnect() - break - - if not reconnect: - yield from self.disconnect() - raise e - - retry = backoff.delay() - log.exception('Disconnected from voice... Reconnecting in %.2fs.', retry) - self._connected.clear() - yield from asyncio.sleep(retry, loop=self.loop) - yield from self.terminate_handshake() - try: - yield from self.connect(reconnect=True) - except asyncio.TimeoutError: - # at this point we've retried 5 times... let's continue the loop. - log.warning('Could not connect to voice... Retrying...') - continue - - @asyncio.coroutine - def disconnect(self, *, force=False): - """|coro| - - Disconnects this voice client from voice. - """ - if not force and not self._connected.is_set(): - return - - self.stop() - self._connected.clear() - - try: - if self.ws: - yield from self.ws.close() - - yield from self.terminate_handshake(remove=True) - finally: - if self.socket: - self.socket.close() - - @asyncio.coroutine - def move_to(self, channel): - """|coro| - - Moves you to a different voice channel. - - Parameters - ----------- - channel: :class:`abc.Snowflake` - The channel to move to. Must be a voice channel. - """ - guild_id, _ = self.channel._get_voice_state_pair() - yield from self.main_ws.voice_state(guild_id, channel.id) - - def is_connected(self): - """bool: Indicates if the voice client is connected to voice.""" - return self._connected.is_set() - - # audio related - - def _get_voice_packet(self, data): - header = bytearray(12) - nonce = bytearray(24) - box = nacl.secret.SecretBox(bytes(self.secret_key)) - - # Formulate header - header[0] = 0x80 - header[1] = 0x78 - struct.pack_into('>H', header, 2, self.sequence) - struct.pack_into('>I', header, 4, self.timestamp) - struct.pack_into('>I', header, 8, self.ssrc) - - # Copy header to nonce's first 12 bytes - nonce[:12] = header - - # Encrypt and return the data - return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext - - def play(self, source, *, after=None): - """Plays an :class:`AudioSource`. - - The finalizer, ``after`` is called after the source has been exhausted - or an error occurred. - - If an error happens while the audio player is running, the exception is - caught and the audio player is then stopped. - - Parameters - ----------- - source: :class:`AudioSource` - The audio source we're reading from. - after - The finalizer that is called after the stream is exhausted. - All exceptions it throws are silently discarded. This function - must have a single parameter, ``error``, that denotes an - optional exception that was raised during playing. - - Raises - ------- - ClientException - Already playing audio or not connected. - TypeError - source is not a :class:`AudioSource` or after is not a callable. - """ - - if not self._connected: - raise ClientException('Not connected to voice.') - - if self.is_playing(): - raise ClientException('Already playing audio.') - - if not isinstance(source, AudioSource): - raise TypeError('source must an AudioSource not {0.__class__.__name__}'.format(source)) - - self._player = AudioPlayer(source, self, after=after) - self._player.start() - - def is_playing(self): - """Indicates if we're currently playing audio.""" - return self._player is not None and self._player.is_playing() - - def is_paused(self): - """Indicates if we're playing audio, but if we're paused.""" - return self._player is not None and self._player.is_paused() - - def stop(self): - """Stops playing audio.""" - if self._player: - self._player.stop() - self._player = None - - def pause(self): - """Pauses the audio playing.""" - if self._player: - self._player.pause() - - def resume(self): - """Resumes the audio playing.""" - if self._player: - self._player.resume() - - @property - def source(self): - """Optional[:class:`AudioSource`]: The audio source being played, if playing. - - This property can also be used to change the audio source currently being played. - """ - return self._player.source if self._player else None - - @source.setter - def source(self, value): - if not isinstance(value, AudioSource): - raise TypeError('expected AudioSource not {0.__class__.__name__}.'.format(value)) - - if self._player is None: - raise ValueError('Not playing anything.') - - self._player._set_source(value) - - def send_audio_packet(self, data, *, encode=True): - """Sends an audio packet composed of the data. - - You must be connected to play audio. - - Parameters - ---------- - data: bytes - The *bytes-like object* denoting PCM or Opus voice data. - encode: bool - Indicates if ``data`` should be encoded into Opus. - - Raises - ------- - ClientException - You are not connected. - OpusError - Encoding the data failed. - """ - - self.checked_add('sequence', 1, 65535) - if encode: - encoded_data = self.encoder.encode(data, self.encoder.SAMPLES_PER_FRAME) - else: - encoded_data = data - packet = self._get_voice_packet(encoded_data) - try: - self.socket.sendto(packet, (self.endpoint_ip, self.voice_port)) - except BlockingIOError: - log.warning('A packet has been dropped (seq: %s, timestamp: %s)', self.sequence, self.timestamp) - - self.checked_add('timestamp', self.encoder.SAMPLES_PER_FRAME, 4294967295) diff --git a/discord.py-rewrite/discord/webhook.py b/discord.py-rewrite/discord/webhook.py deleted file mode 100644 index 4f84987..0000000 --- a/discord.py-rewrite/discord/webhook.py +++ /dev/null @@ -1,659 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import aiohttp -import asyncio -import json -import time -import re - -from . import utils -from .errors import InvalidArgument, HTTPException, Forbidden, NotFound -from .user import BaseUser, User - -__all__ = ('WebhookAdapter', 'AsyncWebhookAdapter', 'RequestsWebhookAdapter', 'Webhook') - -class WebhookAdapter: - """Base class for all webhook adapters. - - Attributes - ------------ - webhook: :class:`Webhook` - The webhook that owns this adapter. - """ - - BASE = 'https://discordapp.com/api/v7' - - def _prepare(self, webhook): - self._webhook_id = webhook.id - self._webhook_token = webhook.token - self._request_url = '{0.BASE}/webhooks/{1}/{2}'.format(self, webhook.id, webhook.token) - self.webhook = webhook - - def request(self, verb, url, payload=None, multipart=None): - """Actually does the request. - - Subclasses must implement this. - - Parameters - ----------- - verb: str - The HTTP verb to use for the request. - url: str - The URL to send the request to. This will have - the query parameters already added to it, if any. - multipart: Optional[dict] - A dict containing multipart form data to send with - the request. If a filename is being uploaded, then it will - be under a ``file`` key which will have a 3-element tuple - denoting ``(filename, file, content_type)``. - payload: Optional[dict] - The JSON to send with the request, if any. - """ - raise NotImplementedError() - - def delete_webhook(self): - return self.request('DELETE', self._request_url) - - def edit_webhook(self, **payload): - return self.request('PATCH', self._request_url, payload=payload) - - def handle_execution_response(self, data, *, wait): - """Transforms the webhook execution response into something - more meaningful. - - This is mainly used to convert the data into a :class:`Message` - if necessary. - - Subclasses must implement this. - - Parameters - ------------ - data - The data that was returned from the request. - wait: bool - Whether the webhook execution was asked to wait or not. - """ - raise NotImplementedError() - - def _store_user(self, data): - # mocks a ConnectionState for appropriate use for Message - return BaseUser(state=self, data=data) - - def execute_webhook(self, *, payload, wait=False, file=None): - if file is not None: - multipart = { - 'file': file, - 'payload_json': utils.to_json(payload) - } - data = None - else: - data = payload - multipart = None - - url = '%s?wait=%d' % (self._request_url, wait) - maybe_coro = self.request('POST', url, multipart=multipart, payload=data) - return self.handle_execution_response(maybe_coro, wait=wait) - -class AsyncWebhookAdapter(WebhookAdapter): - """A webhook adapter suited for use with aiohttp. - - .. note:: - - You are responsible for cleaning up the client session. - - Parameters - ----------- - session: aiohttp.ClientSession - The session to use to send requests. - """ - - def __init__(self, session): - self.session = session - self.loop = session.loop - - @asyncio.coroutine - def request(self, verb, url, payload=None, multipart=None): - headers = {} - data = None - if payload: - headers['Content-Type'] = 'application/json' - data = utils.to_json(payload) - - if multipart: - file = multipart.pop('file', None) - data = aiohttp.FormData() - if file: - data.add_field('file', file[0], filename=file[1], content_type=file[2]) - for key, value in multipart.items(): - data.add_field(key, value) - - for tries in range(5): - r = yield from self.session.request(verb, url, headers=headers, data=data) - try: - data = yield from r.text(encoding='utf-8') - if r.headers['Content-Type'] == 'application/json': - data = json.loads(data) - - # check if we have rate limit header information - remaining = r.headers.get('X-Ratelimit-Remaining') - if remaining == '0' and r.status != 429: - delta = utils._parse_ratelimit_header(r) - yield from asyncio.sleep(delta, loop=self.loop) - - if 300 > r.status >= 200: - return data - - # we are being rate limited - if r.status == 429: - retry_after = data['retry_after'] / 1000.0 - yield from asyncio.sleep(retry_after, loop=self.loop) - continue - - if r.status in (500, 502): - yield from asyncio.sleep(1 + tries * 2, loop=self.loop) - continue - - if r.status == 403: - raise Forbidden(r, data) - elif r.status == 404: - raise NotFound(r, data) - else: - raise HTTPException(r, data) - finally: - yield from r.release() - - @asyncio.coroutine - def handle_execution_response(self, response, *, wait): - data = yield from response - if not wait: - return data - - # transform into Message object - from .message import Message - return Message(data=data, state=self, channel=self.webhook.channel) - -class RequestsWebhookAdapter(WebhookAdapter): - """A webhook adapter suited for use with ``requests``. - - Only versions of requests higher than 2.13.0 are supported. - - Parameters - ----------- - session: Optional[`requests.Session `_] - The requests session to use for sending requests. If not given then - each request will create a new session. Note if a session is given, - the webhook adapter **will not** clean it up for you. You must close - the session yourself. - sleep: bool - Whether to sleep the thread when encountering a 429 or pre-emptive - rate limit or a 5xx status code. Defaults to ``True``. If set to - ``False`` then this will raise an :exc:`HTTPException` instead. - """ - - def __init__(self, session=None, *, sleep=True): - import requests - self.session = session or requests - self.sleep = sleep - - def request(self, verb, url, payload=None, multipart=None): - headers = {} - data = None - if payload: - headers['Content-Type'] = 'application/json' - data = utils.to_json(payload) - - for tries in range(5): - r = self.session.request(verb, url, headers=headers, data=data, files=multipart) - r.encoding = 'utf-8' - data = r.text - - # compatibility with aiohttp - r.status = r.status_code - - if r.headers['Content-Type'] == 'application/json': - data = json.loads(data) - - # check if we have rate limit header information - remaining = r.headers.get('X-Ratelimit-Remaining') - if remaining == '0' and r.status != 429 and self.sleep: - delta = utils._parse_ratelimit_header(r) - time.sleep(delta) - - if 300 > r.status >= 200: - return data - - # we are being rate limited - if r.status == 429: - if self.sleep: - retry_after = data['retry_after'] / 1000.0 - time.sleep(retry_after) - continue - else: - raise HTTPException(r, data) - - if self.sleep and r.status in (500, 502): - time.sleep(1 + tries * 2) - continue - - if r.status == 403: - raise Forbidden(r, data) - elif r.status == 404: - raise NotFound(r, data) - else: - raise HTTPException(r, data) - - def handle_execution_response(self, response, *, wait): - if not wait: - return response - - # transform into Message object - from .message import Message - return Message(data=response, state=self, channel=self.webhook.channel) - -class Webhook: - """Represents a Discord webhook. - - Webhooks are a form to send messages to channels in Discord without a - bot user or authentication. - - There are two main ways to use Webhooks. The first is through the ones - received by the library such as :meth:`.Guild.webhooks` and - :meth:`.TextChannel.webhooks`. The ones received by the library will - automatically have an adapter bound using the library's HTTP session. - Those webhooks will have :meth:`~.Webhook.send`, :meth:`~.Webhook.delete` and - :meth:`~.Webhook.edit` as coroutines. - - The second form involves creating a webhook object manually without having - it bound to a websocket connection using the :meth:`~.Webhook.from_url` or - :meth:`~.Webhook.partial` classmethods. This form allows finer grained control - over how requests are done, allowing you to mix async and sync code using either - ``aiohttp`` or ``requests``. - - For example, creating a webhook from a URL and using ``aiohttp``: - - .. code-block:: python3 - - from discord import Webhook, AsyncWebhookAdapter - import aiohttp - - async def foo(): - async with aiohttp.ClientSession() as session: - webhook = Webhook.from_url('url-here', adapter=AsyncWebhookAdapter(session)) - await webhook.send('Hello World', username='Foo') - - Or creating a webhook from an ID and token and using ``requests``: - - .. code-block:: python3 - - import requests - from discord import Webhook, RequestsWebhookAdapter - - webhook = Webhook.partial(123456, 'abcdefg', adapter=RequestsWebhookAdapter()) - webhook.send('Hello World', username='Foo') - - Attributes - ------------ - id: int - The webhook's ID - token: str - The authentication token of the webhook. - guild_id: Optional[int] - The guild ID this webhook is for. - channel_id: Optional[int] - The channel ID this webhook is for. - user: Optional[:class:`abc.User`] - The user this webhook was created by. If the webhook was - received without authentication then this will be ``None``. - name: Optional[str] - The default name of the webhook. - avatar: Optional[str] - The default avatar of the webhook. - """ - - __slots__ = ('id', 'guild_id', 'channel_id', 'user', 'name', 'avatar', - 'token', '_state', '_adapter') - - def __init__(self, data, *, adapter, state=None): - self.id = int(data['id']) - self.channel_id = utils._get_as_snowflake(data, 'channel_id') - self.guild_id = utils._get_as_snowflake(data, 'guild_id') - self.name = data.get('name') - self.avatar = data.get('avatar') - self.token = data['token'] - self._state = state - self._adapter = adapter - self._adapter._prepare(self) - - user = data.get('user') - if user is None: - self.user = None - elif state is None: - self.user = BaseUser(state=None, data=user) - else: - self.user = User(state=state, data=user) - - def __repr__(self): - return '' % self.id - - @property - def url(self): - """Returns the webhook's url.""" - return 'https://discordapp.com/api/webhooks/{}/{}'.format(self.id, self.token) - - @classmethod - def partial(cls, id, token, *, adapter): - """Creates a partial :class:`Webhook`. - - A partial webhook is just a webhook object with an ID and a token. - - Parameters - ----------- - id: int - The ID of the webhook. - token: str - The authentication token of the webhook. - adapter: :class:`WebhookAdapter` - The webhook adapter to use when sending requests. This is - typically :class:`AsyncWebhookAdapter` for ``aiohttp`` or - :class:`RequestsWebhookAdapter` for ``requests``. - """ - - if not isinstance(adapter, WebhookAdapter): - raise TypeError('adapter must be a subclass of WebhookAdapter') - - data = { - 'id': id, - 'token': token - } - - return cls(data, adapter=adapter) - - @classmethod - def from_url(cls, url, *, adapter): - """Creates a partial :class:`Webhook` from a webhook URL. - - Parameters - ------------ - url: str - The URL of the webhook. - adapter: :class:`WebhookAdapter` - The webhook adapter to use when sending requests. This is - typically :class:`AsyncWebhookAdapter` for ``aiohttp`` or - :class:`RequestsWebhookAdapter` for ``requests``. - - Raises - ------- - InvalidArgument - The URL is invalid. - """ - - m = re.search(r'discordapp.com/api/webhooks/(?P[0-9]{17,21})/(?P[A-Za-z0-9\.\-\_]{60,68})', url) - if m is None: - raise InvalidArgument('Invalid webhook URL given.') - return cls(m.groupdict(), adapter=adapter) - - @classmethod - def from_state(cls, data, state): - return cls(data, adapter=AsyncWebhookAdapter(session=state.http._session), state=state) - - @property - def guild(self): - """Optional[:class:`Guild`]: The guild this webhook belongs to. - - If this is a partial webhook, then this will always return ``None``. - """ - return self._state and self._state.get_guild(self.guild_id) - - @property - def channel(self): - """Optional[:class:`TextChannel`]: The text channel this webhook belongs to. - - If this is a partial webhook, then this will always return ``None``. - """ - guild = self.guild - return guild and guild.get_channel(self.channel_id) - - @property - def created_at(self): - """Returns the webhook's creation time in UTC.""" - return utils.snowflake_time(self.id) - - @property - def avatar_url(self): - """Returns a friendly URL version of the avatar the webhook has. - - If the webhook does not have a traditional avatar, their default - avatar URL is returned instead. - - This is equivalent to calling :meth:`avatar_url_as` with the - default parameters. - """ - return self.avatar_url_as() - - def avatar_url_as(self, *, format=None, size=1024): - """Returns a friendly URL version of the avatar the webhook has. - - If the webhook does not have a traditional avatar, their default - avatar URL is returned instead. - - The format must be one of 'jpeg', 'jpg', or 'png'. - The size must be a power of 2 between 16 and 1024. - - Parameters - ----------- - format: Optional[str] - The format to attempt to convert the avatar to. - If the format is ``None``, then it is equivalent to png. - size: int - The size of the image to display. - - Returns - -------- - str - The resulting CDN URL. - - Raises - ------ - InvalidArgument - Bad image format passed to ``format`` or invalid ``size``. - """ - if self.avatar is None: - # Default is always blurple apparently - return 'https://cdn.discordapp.com/embed/avatars/0.png' - - if not utils.valid_icon_size(size): - raise InvalidArgument("size must be a power of 2 between 16 and 1024") - - format = format or 'png' - - if format not in ('png', 'jpg', 'jpeg'): - raise InvalidArgument("format must be one of 'png', 'jpg', or 'jpeg'.") - - return 'https://cdn.discordapp.com/avatars/{0.id}/{0.avatar}.{1}?size={2}'.format(self, format, size) - - def delete(self): - """|maybecoro| - - Deletes this Webhook. - - If the webhook is constructed with a :class:`RequestsWebhookAdapter` then this is - not a coroutine. - - Raises - ------- - HTTPException - Deleting the webhook failed. - NotFound - This webhook does not exist. - Forbidden - You do not have permissions to delete this webhook. - """ - return self._adapter.delete_webhook(self.id, self.token) - - def edit(self, **kwargs): - """|maybecoro| - - Edits this Webhook. - - If the webhook is constructed with a :class:`RequestsWebhookAdapter` then this is - not a coroutine. - - Parameters - ------------- - name: Optional[str] - The webhook's new default name. - avatar: Optional[bytes] - A *bytes-like* object representing the webhook's new default avatar. - - Raises - ------- - HTTPException - Editing the webhook failed. - NotFound - This webhook does not exist. - Forbidden - You do not have permissions to edit this webhook. - """ - payload = {} - - try: - name = kwargs['name'] - except KeyError: - pass - else: - if name is not None: - payload['name'] = str(name) - else: - payload['name'] = None - - try: - avatar = kwargs['avatar'] - except KeyError: - pass - else: - if avatar is not None: - payload['avatar'] = utils._bytes_to_base64_data(avatar) - else: - payload['avatar'] = None - - return self._adapter.edit_webhook(**payload) - - def send(self, content=None, *, wait=False, username=None, avatar_url=None, - tts=False, file=None, embed=None, embeds=None): - """|maybecoro| - - Sends a message using the webhook. - - If the webhook is constructed with a :class:`RequestsWebhookAdapter` then this is - not a coroutine. - - The content must be a type that can convert to a string through ``str(content)``. - - To upload a single file, the ``file`` parameter should be used with a - single :class:`File` object. - - If the ``embed`` parameter is provided, it must be of type :class:`Embed` and - it must be a rich embed type. You cannot mix the ``embed`` parameter with the - ``embeds`` parameter, which must be a list of :class:`Embed` objects to send. - - Parameters - ------------ - content - The content of the message to send. - wait: bool - Whether the server should wait before sending a response. This essentially - means that the return type of this function changes from ``None`` to - a :class:`Message` if set to ``True``. - username: str - The username to send with this message. If no username is provided - then the default username for the webhook is used. - avatar_url: str - The avatar URL to send with this message. If no avatar URL is provided - then the default avatar for the webhook is used. - tts: bool - Indicates if the message should be sent using text-to-speech. - file: :class:`File` - The file to upload. - embed: :class:`Embed` - The rich embed for the content to send. This cannot be mixed with - ``embeds`` parameter. - embeds: List[:class:`Embed`] - A list of embeds to send with the content. Maximum of 10. This cannot - be mixed with the ``embed`` parameter. - - Raises - -------- - HTTPException - Sending the message failed. - NotFound - This webhook was not found. - Forbidden - The authorization token for the webhook is incorrect. - InvalidArgument - You specified both ``embed`` and ``embeds`` or the length of - ``embeds`` was invalid. - - Returns - --------- - Optional[:class:`Message`] - The message that was sent. - """ - - payload = {} - - if embeds is not None and embed is not None: - raise InvalidArgument('Cannot mix embed and embeds keyword arguments.') - - if embeds is not None: - if len(embeds) > 10: - raise InvalidArgument('embeds has a maximum of 10 elements.') - payload['embeds'] = [e.to_dict() for e in embeds] - - if embed is not None: - payload['embeds'] = [embed.to_dict()] - - if content is not None: - payload['content'] = str(content) - - payload['tts'] = tts - if avatar_url: - payload['avatar_url'] = avatar_url - if username: - payload['username'] = username - - if file is not None: - try: - to_pass = (file.open_file(), file.filename, 'application/octet-stream') - return self._adapter.execute_webhook(wait=wait, file=to_pass, payload=payload) - finally: - file.close() - else: - return self._adapter.execute_webhook(wait=wait, payload=payload) - - def execute(self, *args, **kwargs): - """An alias for :meth:`~.Webhook.send`.""" - return self.send(*args, **kwargs) diff --git a/discord.py-rewrite/docs/Makefile b/discord.py-rewrite/docs/Makefile deleted file mode 100644 index 05ff4d9..0000000 --- a/discord.py-rewrite/docs/Makefile +++ /dev/null @@ -1,183 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/discord.py.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/discord.py.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/discord.py" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/discord.py" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/discord.py-rewrite/docs/_static/custom.js b/discord.py-rewrite/docs/_static/custom.js deleted file mode 100644 index 235a14e..0000000 --- a/discord.py-rewrite/docs/_static/custom.js +++ /dev/null @@ -1,31 +0,0 @@ -$(document).ready(function () { - var sections = $('div.section'); - var activeLink = null; - var bottomHeightThreshold = $(document).height() - 30; - - $(window).scroll(function (event) { - var distanceFromTop = $(this).scrollTop(); - var currentSection = null; - - if(distanceFromTop + window.innerHeight > bottomHeightThreshold) { - currentSection = $(sections[sections.length - 1]); - } - else { - sections.each(function () { - var section = $(this); - if (section.offset().top - 1 < distanceFromTop) { - currentSection = section; - } - }); - } - - if (activeLink) { - activeLink.parent().removeClass('active'); - } - - if (currentSection) { - activeLink = $('.sphinxsidebar a[href="#' + currentSection.attr('id') + '"]'); - activeLink.parent().addClass('active'); - } - }); -}); diff --git a/discord.py-rewrite/docs/_static/style.css b/discord.py-rewrite/docs/_static/style.css deleted file mode 100644 index 16067d1..0000000 --- a/discord.py-rewrite/docs/_static/style.css +++ /dev/null @@ -1,542 +0,0 @@ -/* this stuff uses a couple of themes as a base with some custom stuff added - -In particular thanks to: - -- Alabaster for being a good base - - Which thanks Flask + KR theme -- Sphinx Readable Theme - - Which also proved to be a great base -*/ - -@import url('basic.css'); - -body { - font-family: 'Georgia', serif; - font-size: 16px; - margin: 0; - padding: 0; -} - -p { - margin-bottom: 8px; -} - -div.document { - margin: 10px auto 0 auto; - max-width: 940px; /* page width */ -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 220px; /* sidebar width */ -} - -div.body { - background-color: #ffffff; - color: #3e4349; - padding: 0 30px 30px 30px; -} - -div.footer { - color: #555; - font-size: 14px; - margin: 20px auto 30px auto; - text-align: right; - max-width: 880px; -} - -div.footer a { - color: #444; - text-decoration: underline; -} - -div.related { - padding: 10px 10px; - width: auto; -} - -div.sphinxsidebar { - float: left; - font-size: 14px; - line-height: 1.5em; - margin-left: -100%; - width: 220px; /* sidebar width */ -} - -div.sphinxsidebarwrapper { - font-size: 14px; - line-height: 1.5em; - padding: 10px 0 10px 10px; - - /* sticky sidebar */ - position: fixed; - width: 220px; /* sidebar width */ - height: 90%; - overflow: hidden; -} - -/* show scrollbar on hover */ -div.sphinxsidebarwrapper:hover { - overflow: auto; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - color: #333; - font-size: 24px; - font-weight: normal; - margin: 0 0 5px 0; - padding: 0; -} - -div.sphinxsidebar h4 { - font-size: 1.1em; -} - -div.sphinxsidebar h3 a { - color: #333; -} - -div.sphinxsidebar p { - color: #888; -} - -div.sphinxsidebar p.searchtip { - line-height: 1.4em; -} - -div.sphinxsidebar ul { - color: #000; - margin: 10px 0 20px; - padding: 0; -} - -div.sphinxsidebar a { - color: #444; -} - -div.sphinxsidebar input { - border: 1px solid #ccc; - font-family: sans-serif; - font-size: 1em; - margin-top: 10px; -} - -/* -- body styles --------------------------------------------------------- */ - -a { - color: #2591c4; - text-decoration: none; -} - -a:hover { - color: #0b3a44; - text-decoration: underline; -} - -hr { - border: 1px solid #b1b4b6; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { font-weight: normal; } - -div.body h1, -div.body h2, -div.body h3, -div.body h4 { color: #212224; } -div.body h5 { color: #000; } -div.body h6 { color: #777; } - -div.body h1 { margin: 0 0 10px 0; } -div.body h2, -div.body h3 { margin: 10px 0px 10px 0px; } -div.body h4, -div.body h5, -div.body h6 { margin: 20px 0px 10px 0px; } - -div.body h1 { padding: 0 0 10px 0; } -div.body h2, -div.body h3 { padding: 10px 0 10px 0; } -div.body h4 { padding: 10px 0 10px 0; } -div.body h5, -div.body h6 { padding: 10px 0 0 0; } - -div.body h1, -div.body h2, -div.body h3 { border-bottom: 1px solid #ddd; } -div.body h4 { border-bottom: 1px solid #e5e5e5; } - -div.body h1 { font-size: 230%; } -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 130%; } -div.body h4 { font-size: 110%; } -div.body h5 { font-size: 105%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #3e4349; - font-size: 0.8em; - padding: 0 4px 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - background-color: #3e4349; - color: #fff; -} - -div.body ul { - list-style: disc; - margin: 1em 0; - padding-left: 1.3em; -} - -div.body ul ul, div.body ol ul { - margin: .2em 0; - padding-left: 1.2em; -} - -div.body ul li { - padding: 2px 0; -} - -div.body ul.search li { - padding: 5px 0 5px 20px; -} - -div.body ol { - counter-reset: li; - margin-left: 0; - padding-left: 0; -} - -div.body ol ol { - margin: .2em 0; -} - -div.body ol > li { - list-style: none; - margin: 0 0 0 1.9em; - padding: 2px 1px; - position: relative; -} - -div.body ol > li:before { - content: counter(li) "."; - counter-increment: li; - top: -2px; - left: -1.9em; - width: 1.9em; - padding: 4px 0; - position: absolute; - text-align: left; -} - -div.body p, -div.body dd, -div.body li { - line-height: 1.4em; -} - -/* weird margins */ -li > p { - margin: 2px; -} - -li > blockquote { - margin: 10px; -} - -div.admonition p.admonition-title + p { - display: inline; -} - -div.highlight { - background-color: #fff; -} - -div.important, div.note, div.hint, div.tip { - background-color: #eee; - border: 1px solid #ccc; -} - -div.attention, div.warning, div.caution, div.seealso { - background-color: #fef9e9; - border: 1px solid #fbe091; -} - -/* no disgusting background in the FAQ */ -div.topic { - background-color: transparent; - border: none; -} - -/* don't link-ify the FAQ page */ -a.toc-backref { - text-decoration: none; - color: #3e4349; -} - -/* bold and fix the Parameter, Raises, etc. */ -dl.field-list > dt { - font-weight: bold; -} - -div.danger, div.error { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -div.admonition { - padding: 10px; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ':'; -} - -/* helpful admonitions */ -div.helpful { - background-color: #e4f2ff; - border: 1px solid #66b3ff; -} - -div.helpful > p.admonition-title { - display: block; -} - -div.helpful > p.admonition-title:after { - content: unset; -} - -pre { - background-color: #f5f5f5; - border: 1px solid #C6C9CB; - color: #222; - font-size: 0.75em; - line-height: 1.5em; - margin: 1.5em 0 1.5em 0; - padding: 10px; -} - -pre, tt, code { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.9em; -} - -tt, code { - background-color: #ecf0f3; -} - -tt.descname, code.descname { - font-size: 0.95em; -} - -tt.xref, a tt, code.xref, a code { - font-weight: normal; -} - -span.pre { - padding: 0 2px; -} - -dl.class { - margin-bottom: 50px; -} - -dl.describe > dt, -dl.function > dt, -dl.attribute > dt, -dl.classmethod > dt, -dl.method > dt, -dl.class > dt, -dl.exception > dt { - background-color: #f5f5f5; - padding: 1px 10px; -} - -dd { - margin-top: 10px; -} - - -.container.operations { - padding: 10px; - border: 1px solid #ddd; - margin-bottom: 20px; -} - -.container.operations::before { - content: 'Supported Operations'; - color: #212224; - display: block; - padding-bottom: 5px; -} - -.container.operations > dl.describe > dt { - background-color: #f8f8f8; -} - -table.docutils { - width: 100%; -} - -table.docutils.footnote { - width: auto; -} - -table.docutils thead, -table.docutils tfoot { - background: #f5f5f5; -} - -table.docutils thead tr th { - color: #000; - font-weight: normal; - padding: 7px 5px; - vertical-align: middle; -} - -table.docutils tbody tr th, -table.docutils tbody tr td { - border-bottom: 0; - border-top: solid 1px #ddd; - padding: 7px 5px; - vertical-align: top; -} -table.docutils tbody tr:last-child th, -table.docutils tbody tr:last-child td { - border-bottom: solid 1px #ddd; -} - -table.docutils thead tr td p, -table.docutils tfoot tr td p, -table.docutils tbody tr td p, -table.docutils thead tr td ul, -table.docutils tfoot tr td ul, -table.docutils tbody tr td ul, -table.docutils thead tr td ol, -table.docutils tfoot tr td ol, -table.docutils tbody tr td ol { - margin: 0 0 .5em; -} -table.docutils thead tr td p.last, -table.docutils tfoot tr td p.last, -table.docutils tbody tr td p.last, -table.docutils thead tr td ul.last, -table.docutils tfoot tr td ul.last, -table.docutils tbody tr td ul.last, -table.docutils thead tr td ol.last, -table.docutils tfoot tr td ol.last, -table.docutils tbody tr td ol.last { - margin-bottom: 0; -} - -.viewcode-back { - font-family: Arial, sans-serif; -} - -div.viewcode-block:target { - background-color: #fef9e9; - border-top: 1px solid #fbe091; - border-bottom: 1px solid #fbe091; -} - -/* hide the welcome text */ -div#welcome-to-discord-py > h1 { - display: none; -} - -.active { - background-color: #dbdbdb; - border-left: 5px solid #dbdbdb; -} - -@media screen and (max-width: 870px) { - - div.document { - width: auto; - margin: 0; - } - - div.documentwrapper { - float: none; - } - - div.bodywrapper { - margin: 0; - } - - div.body { - min-height: 0; - padding: 0 20px 30px 20px; - } - - div.footer { - background-color: #333; - color: #888; - margin: 0; - padding: 10px 20px 20px; - text-align: left; - width: auto; - } - - div.footer a { - color: #bbb; - } - - div.footer a:hover { - color: #fff; - } - - div.sphinxsidebar { - background-color: #333; - color: #fff; - float: none; - margin: 0; - padding: 10px 20px; - width: auto; - } - - /* sticky sidebar */ - div.sphinxsidebarwrapper { - position: relative; - } - - div.sphinxsidebar h3, - div.sphinxsidebar h4, - div.sphinxsidebar p, - div.sphinxsidebar h3 a { - color: #fff; - } - - div.sphinxsidebar ul { - color: #999; - } - - div.sphinxsidebar a { - color: #aaa; - } - - div.sphinxsidebar a:hover { - color: #fff; - } - - .active { - background-color: transparent; - border-left: none; - } -} diff --git a/discord.py-rewrite/docs/_templates/layout.html b/discord.py-rewrite/docs/_templates/layout.html deleted file mode 100644 index 6816150..0000000 --- a/discord.py-rewrite/docs/_templates/layout.html +++ /dev/null @@ -1,29 +0,0 @@ -{%- extends "basic/layout.html" %} - -{% set show_source = False %} -{% set style = 'style.css' %} - -{%- block extrahead %} - {{ super() }} - -{% endblock %} - -{%- block relbar2 %}{% endblock %} - -{% block header %} - {{ super() }} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} - - -{%- block footer %} - - {% if pagename == 'index' %} -
- {% endif %} -{%- endblock %} diff --git a/discord.py-rewrite/docs/_templates/relations.html b/discord.py-rewrite/docs/_templates/relations.html deleted file mode 100644 index db904e8..0000000 --- a/discord.py-rewrite/docs/_templates/relations.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/discord.py-rewrite/docs/api.rst b/discord.py-rewrite/docs/api.rst deleted file mode 100644 index 6e0f52b..0000000 --- a/discord.py-rewrite/docs/api.rst +++ /dev/null @@ -1,2047 +0,0 @@ -.. currentmodule:: discord - -API Reference -=============== - -The following section outlines the API of discord.py. - -.. note:: - - This module uses the Python logging module to log diagnostic and errors - in an output independent way. If the logging module is not configured, - these logs will not be output anywhere. See :ref:`logging_setup` for - more information on how to set up and use the logging module with - discord.py. - -Version Related Info ---------------------- - -There are two main ways to query version information about the library. - -.. data:: version_info - - A named tuple that is similar to `sys.version_info`_. - - Just like `sys.version_info`_ the valid values for ``releaselevel`` are - 'alpha', 'beta', 'candidate' and 'final'. - - .. _sys.version_info: https://docs.python.org/3.5/library/sys.html#sys.version_info - -.. data:: __version__ - - A string representation of the version. e.g. ``'0.10.0-alpha0'``. - -Client -------- - -.. autoclass:: Client - :members: - -.. autoclass:: AutoShardedClient - :members: - -Voice ------- - -.. autoclass:: VoiceClient - :members: - -.. autoclass:: AudioSource - :members: - -.. autoclass:: PCMAudio - :members: - -.. autoclass:: FFmpegPCMAudio - :members: - -.. autoclass:: PCMVolumeTransformer - :members: - -Opus Library -~~~~~~~~~~~~~ - -.. autofunction:: discord.opus.load_opus - -.. autofunction:: discord.opus.is_loaded - -.. _discord-api-events: - -Event Reference ---------------- - -This page outlines the different types of events listened by :class:`Client`. - -There are two ways to register an event, the first way is through the use of -:meth:`Client.event`. The second way is through subclassing :class:`Client` and -overriding the specific events. For example: :: - - import discord - - class MyClient(discord.Client): - async def on_message(self, message): - if message.author != self.user: - return - - if message.content.startswith('$hello'): - await message.channel.send('Hello World!') - - -If an event handler raises an exception, :func:`on_error` will be called -to handle it, which defaults to print a traceback and ignoring the exception. - -.. warning:: - - All the events must be a |corourl|_. If they aren't, then you might get unexpected - errors. In order to turn a function into a coroutine they must either be ``async def`` - functions or in 3.4 decorated with ``@asyncio.coroutine``. - - The following two functions are examples of coroutine functions: :: - - async def on_ready(): - pass - - @asyncio.coroutine - def on_ready(): - pass - -.. function:: on_connect() - - Called when the client has successfully connected to Discord. This is not - the same as the client being fully prepared, see :func:`on_ready` for that. - - The warnings on :func:`on_ready` also apply. - -.. function:: on_ready() - - Called when the client is done preparing the data received from Discord. Usually after login is successful - and the :attr:`Client.guilds` and co. are filled up. - - .. warning:: - - This function is not guaranteed to be the first event called. - Likewise, this function is **not** guaranteed to only be called - once. This library implements reconnection logic and thus will - end up calling this event whenever a RESUME request fails. - -.. function:: on_shard_ready(shard_id) - - Similar to :func:`on_ready` except used by :class:`AutoShardedClient` - to denote when a particular shard ID has become ready. - - :param shard_id: The shard ID that is ready. - -.. function:: on_resumed() - - Called when the client has resumed a session. - -.. function:: on_error(event, \*args, \*\*kwargs) - - Usually when an event raises an uncaught exception, a traceback is - printed to stderr and the exception is ignored. If you want to - change this behaviour and handle the exception for whatever reason - yourself, this event can be overridden. Which, when done, will - supress the default action of printing the traceback. - - The information of the exception rasied and the exception itself can - be retreived with a standard call to ``sys.exc_info()``. - - If you want exception to propogate out of the :class:`Client` class - you can define an ``on_error`` handler consisting of a single empty - ``raise`` statement. Exceptions raised by ``on_error`` will not be - handled in any way by :class:`Client`. - - :param event: The name of the event that raised the exception. - :param args: The positional arguments for the event that raised the - exception. - :param kwargs: The keyword arguments for the event that raised the - execption. - -.. function:: on_socket_raw_receive(msg) - - Called whenever a message is received from the WebSocket, before - it's processed. This event is always dispatched when a message is - received and the passed data is not processed in any way. - - This is only really useful for grabbing the WebSocket stream and - debugging purposes. - - .. note:: - - This is only for the messages received from the client - WebSocket. The voice WebSocket will not trigger this event. - - :param msg: The message passed in from the WebSocket library. - Could be ``bytes`` for a binary message or ``str`` - for a regular message. - -.. function:: on_socket_raw_send(payload) - - Called whenever a send operation is done on the WebSocket before the - message is sent. The passed parameter is the message that is being - sent to the WebSocket. - - This is only really useful for grabbing the WebSocket stream and - debugging purposes. - - .. note:: - - This is only for the messages received from the client - WebSocket. The voice WebSocket will not trigger this event. - - :param payload: The message that is about to be passed on to the - WebSocket library. It can be ``bytes`` to denote a binary - message or ``str`` to denote a regular text message. - -.. function:: on_typing(channel, user, when) - - Called when someone begins typing a message. - - The ``channel`` parameter can be a :class:`abc.Messageable` instance. - Which could either be :class:`TextChannel`, :class:`GroupChannel`, or - :class:`DMChannel`. - - If the ``channel`` is a :class:`TextChannel` then the ``user`` parameter - is a :class:`Member`, otherwise it is a :class:`User`. - - :param channel: The location where the typing originated from. - :param user: The user that started typing. - :param when: A ``datetime.datetime`` object representing when typing started. - -.. function:: on_message(message) - - Called when a :class:`Message` is created and sent. - - .. warning:: - - Your bot's own messages and private messages are sent through this - event. This can lead cases of 'recursion' depending on how your bot was - programmed. If you want the bot to not reply to itself, consider - checking the user IDs. Note that :class:`~ext.commands.Bot` does not - have this problem. - - :param message: A :class:`Message` of the current message. - -.. function:: on_message_delete(message) - - Called when a message is deleted. If the message is not found in the - :attr:`Client.messages` cache, then these events will not be called. This - happens if the message is too old or the client is participating in high - traffic guilds. To fix this, increase the ``max_messages`` option of - :class:`Client`. - - :param message: A :class:`Message` of the deleted message. - -.. function:: on_raw_message_delete(message_id, channel_id) - - Called when a message is deleted. Unlike :func:`on_message_delete`, this is - called regardless of the message being in the internal message cache or not. - - :param int message_id: The message ID of the message being deleted. - :param int channel_id: The channel ID where the message was deleted. - -.. function:: on_raw_bulk_message_delete(message_ids, channel_id) - - Called when a bulk delete is triggered. This event is called regardless - of the message IDs being in the internal message cache or not. - - :param message_ids: The message IDs that were bulk deleted. - :type message_ids: Set[int] - :param int channel_id: The channel ID where the messages were deleted. - -.. function:: on_message_edit(before, after) - - Called when a :class:`Message` receives an update event. If the message is not found - in the :attr:`Client.messages` cache, then these events will not be called. - This happens if the message is too old or the client is participating in high - traffic guilds. To fix this, increase the ``max_messages`` option of :class:`Client`. - - The following non-exhaustive cases trigger this event: - - - A message has been pinned or unpinned. - - The message content has been changed. - - The message has received an embed. - - - For performance reasons, the embed server does not do this in a "consistent" manner. - - - A call message has received an update to its participants or ending time. - - :param before: A :class:`Message` of the previous version of the message. - :param after: A :class:`Message` of the current version of the message. - -.. function:: on_raw_message_edit(message_id, data) - - Called when a message is edited. Unlike :func:`on_message_edit`, this is called - regardless of the state of the internal message cache. - - Due to the inherently raw nature of this event, the data parameter coincides with - the raw data given by the `gateway `_ - - Since the data payload can be partial, care must be taken when accessing stuff in the dictionary. - One example of a common case of partial data is when the ``'content'`` key is inaccessible. This - denotes an "embed" only edit, which is an edit in which only the embeds are updated by the Discord - embed server. - - :param int message_id: The message ID of the message being edited. - :param dict data: The raw data being passed to the MESSAGE_UPDATE gateway event. - -.. function:: on_reaction_add(reaction, user) - - Called when a message has a reaction added to it. Similar to on_message_edit, - if the message is not found in the :attr:`Client.messages` cache, then this - event will not be called. - - .. note:: - - To get the :class:`Message` being reacted, access it via :attr:`Reaction.message`. - - :param reaction: A :class:`Reaction` showing the current state of the reaction. - :param user: A :class:`User` or :class:`Member` of the user who added the reaction. - -.. function:: on_raw_reaction_add(emoji, message_id, channel_id, user_id) - - Called when a reaction has a reaction added. Unlike :func:`on_reaction_add`, this is - called regardless of the state of the internal message cache. - - :param emoji: The custom or unicode emoji being reacted to. - :type emoji: :class:`PartialReactionEmoji` - :param int message_id: The message ID of the message being reacted. - :param int channel_id: The channel ID where the message belongs to. - :param int user_id: The user ID of the user who did the reaction. - -.. function:: on_reaction_remove(reaction, user) - - Called when a message has a reaction removed from it. Similar to on_message_edit, - if the message is not found in the :attr:`Client.messages` cache, then this event - will not be called. - - .. note:: - - To get the message being reacted, access it via :attr:`Reaction.message`. - - :param reaction: A :class:`Reaction` showing the current state of the reaction. - :param user: A :class:`User` or :class:`Member` of the user who removed the reaction. - -.. function:: on_raw_reaction_remove(emoji, message_id, channel_id, user_id) - - Called when a reaction has a reaction removed. Unlike :func:`on_reaction_remove`, this is - called regardless of the state of the internal message cache. - - :param emoji: The custom or unicode emoji that got un-reacted. - :type emoji: :class:`PartialReactionEmoji` - :param int message_id: The message ID of the message being un-reacted. - :param int channel_id: The channel ID where the message belongs to. - :param int user_id: The user ID of the user who removed the reaction. - -.. function:: on_reaction_clear(message, reactions) - - Called when a message has all its reactions removed from it. Similar to :func:`on_message_edit`, - if the message is not found in the :attr:`Client.messages` cache, then this event - will not be called. - - :param message: The :class:`Message` that had its reactions cleared. - :param reactions: A list of :class:`Reaction`\s that were removed. - -.. function:: on_raw_reaction_clear(message_id, channel_id) - - Called when a message has all its reactions removed. Unlike :func:`on_reaction_clear`, - this is called regardless of the state of the internal message cache. - - :param int message_id: The message ID of the message having its reactions removed. - :param int channel_id: The channel ID of where the message belongs to. - -.. function:: on_private_channel_delete(channel) - on_private_channel_create(channel) - - Called whenever a private channel is deleted or created. - - :param channel: The :class:`abc.PrivateChannel` that got created or deleted. - -.. function:: on_private_channel_update(before, after) - - Called whenever a private group DM is updated. e.g. changed name or topic. - - :param before: The :class:`GroupChannel` that got updated with the old info. - :param after: The :class:`GroupChannel` that got updated with the updated info. - -.. function:: on_private_channel_pins_update(channel, last_pin) - - Called whenever a message is pinned or unpinned from a private channel. - - :param channel: The :class:`abc.PrivateChannel` that had it's pins updated. - :param last_pin: A ``datetime.datetime`` object representing when the latest message - was pinned or ``None`` if there are no pins. - -.. function:: on_guild_channel_delete(channel) - on_guild_channel_create(channel) - - Called whenever a guild channel is deleted or created. - - Note that you can get the guild from :attr:`~abc.GuildChannel.guild`. - - :param channel: The :class:`abc.GuildChannel` that got created or deleted. - -.. function:: on_guild_channel_update(before, after) - - Called whenever a guild channel is updated. e.g. changed name, topic, permissions. - - :param before: The :class:`abc.GuildChannel` that got updated with the old info. - :param after: The :class:`abc.GuildChannel` that got updated with the updated info. - -.. function:: on_guild_channel_pins_update(channel, last_pin) - - Called whenever a message is pinned or unpinned from a guild channel. - - :param channel: The :class:`abc.GuildChannel` that had it's pins updated. - :param last_pin: A ``datetime.datetime`` object representing when the latest message - was pinned or ``None`` if there are no pins. - -.. function:: on_member_join(member) - on_member_remove(member) - - Called when a :class:`Member` leaves or joins a :class:`Guild`. - - :param member: The :class:`Member` that joined or left. - -.. function:: on_member_update(before, after) - - Called when a :class:`Member` updates their profile. - - This is called when one or more of the following things change: - - - status - - game playing - - avatar - - nickname - - roles - - :param before: The :class:`Member` that updated their profile with the old info. - :param after: The :class:`Member` that updated their profile with the updated info. - -.. function:: on_guild_join(guild) - - Called when a :class:`Guild` is either created by the :class:`Client` or when the - :class:`Client` joins a guild. - - :param guild: The :class:`Guild` that was joined. - -.. function:: on_guild_remove(guild) - - Called when a :class:`Guild` is removed from the :class:`Client`. - - This happens through, but not limited to, these circumstances: - - - The client got banned. - - The client got kicked. - - The client left the guild. - - The client or the guild owner deleted the guild. - - In order for this event to be invoked then the :class:`Client` must have - been part of the guild to begin with. (i.e. it is part of :attr:`Client.guilds`) - - :param guild: The :class:`Guild` that got removed. - -.. function:: on_guild_update(before, after) - - Called when a :class:`Guild` updates, for example: - - - Changed name - - Changed AFK channel - - Changed AFK timeout - - etc - - :param before: The :class:`Guild` prior to being updated. - :param after: The :class:`Guild` after being updated. - -.. function:: on_guild_role_create(role) - on_guild_role_delete(role) - - Called when a :class:`Guild` creates or deletes a new :class:`Role`. - - To get the guild it belongs to, use :attr:`Role.guild`. - - :param role: The :class:`Role` that was created or deleted. - -.. function:: on_guild_role_update(before, after) - - Called when a :class:`Role` is changed guild-wide. - - :param before: The :class:`Role` that updated with the old info. - :param after: The :class:`Role` that updated with the updated info. - -.. function:: on_guild_emojis_update(guild, before, after) - - Called when a :class:`Guild` adds or removes :class:`Emoji`. - - :param guild: The :class:`Guild` who got their emojis updated. - :param before: A list of :class:`Emoji` before the update. - :param after: A list of :class:`Emoji` after the update. - -.. function:: on_guild_available(guild) - on_guild_unavailable(guild) - - Called when a guild becomes available or unavailable. The guild must have - existed in the :attr:`Client.guilds` cache. - - :param guild: The :class:`Guild` that has changed availability. - -.. function:: on_voice_state_update(member, before, after) - - Called when a :class:`Member` changes their :class:`VoiceState`. - - The following, but not limited to, examples illustrate when this event is called: - - - A member joins a voice room. - - A member leaves a voice room. - - A member is muted or deafened by their own accord. - - A member is muted or deafened by a guild administrator. - - :param member: The :class:`Member` whose voice states changed. - :param before: The :class:`VoiceState` prior to the changes. - :param after: The :class:`VoiceState` after to the changes. - -.. function:: on_member_ban(guild, user) - - Called when user gets banned from a :class:`Guild`. - - :param guild: The :class:`Guild` the user got banned from. - :param user: The user that got banned. - Can be either :class:`User` or :class:`Member` depending if - the user was in the guild or not at the time of removal. - -.. function:: on_member_unban(guild, user) - - Called when a :class:`User` gets unbanned from a :class:`Guild`. - - :param guild: The :class:`Guild` the user got unbanned from. - :param user: The :class:`User` that got unbanned. - -.. function:: on_group_join(channel, user) - on_group_remove(channel, user) - - Called when someone joins or leaves a group, i.e. a :class:`PrivateChannel` - with a :attr:`PrivateChannel.type` of :attr:`ChannelType.group`. - - :param channel: The group that the user joined or left. - :param user: The user that joined or left. - -.. function:: on_relationship_add(relationship) - on_relationship_remove(relationship) - - Called when a :class:`Relationship` is added or removed from the - :class:`ClientUser`. - - :param relationship: The relationship that was added or removed. - -.. function:: on_relationship_update(before, after) - - Called when a :class:`Relationship` is updated, e.g. when you - block a friend or a friendship is accepted. - - :param before: The previous relationship status. - :param after: The updated relationship status. - -.. _discord-api-utils: - -Utility Functions ------------------ - -.. autofunction:: discord.utils.find - -.. autofunction:: discord.utils.get - -.. autofunction:: discord.utils.snowflake_time - -.. autofunction:: discord.utils.oauth_url - -Application Info ------------------- - -.. class:: AppInfo - - A namedtuple representing the bot's application info. - - .. attribute:: id - - The application's ``client_id``. - .. attribute:: name - - The application's name. - .. attribute:: description - - The application's description - .. attribute:: icon - - The application's icon hash if it exists, ``None`` otherwise. - .. attribute:: icon_url - - A property that retrieves the application's icon URL if it exists. - - If it doesn't exist an empty string is returned. - .. attribute:: owner - - The owner of the application. This is a :class:`User` instance - with the owner's information at the time of the call. - -Profile ---------- - -.. class:: Profile - - A namedtuple representing a user's Discord public profile. - - .. attribute:: user - - The :class:`User` the profile belongs to. - .. attribute:: premium - - A boolean indicating if the user has premium (i.e. Discord Nitro). - .. attribute:: nitro - - An alias for :attr:`premium`. - .. attribute:: premium_since - - A naive UTC datetime indicating how long the user has been premium since. - This could be ``None`` if not applicable. - .. attribute:: staff - - A boolean indicating if the user is Discord Staff. - .. attribute:: partner - - A boolean indicating if the user is a Discord Partner. - .. attribute:: hypesquad - - A boolean indicating if the user is in Discord HypeSquad. - .. attribute:: mutual_guilds - - A list of :class:`Guild` that the :class:`ClientUser` shares with this - user. - .. attribute:: connected_accounts - - A list of dict objects indicating the accounts the user has connected. - - An example entry can be seen below: :: - - {type: "twitch", id: "92473777", name: "discordapp"} - -.. _discord-api-enums: - -Enumerations -------------- - -The API provides some enumerations for certain types of strings to avoid the API -from being stringly typed in case the strings change in the future. - -All enumerations are subclasses of `enum`_. - -.. _enum: https://docs.python.org/3/library/enum.html - -.. class:: ChannelType - - Specifies the type of channel. - - .. attribute:: text - - A text channel. - .. attribute:: voice - - A voice channel. - .. attribute:: private - - A private text channel. Also called a direct message. - .. attribute:: group - - A private group text channel. - -.. class:: MessageType - - Specifies the type of :class:`Message`. This is used to denote if a message - is to be interpreted as a system message or a regular message. - - .. attribute:: default - - The default message type. This is the same as regular messages. - .. attribute:: recipient_add - - The system message when a recipient is added to a group private - message, i.e. a private channel of type :attr:`ChannelType.group`. - .. attribute:: recipient_remove - - The system message when a recipient is removed from a group private - message, i.e. a private channel of type :attr:`ChannelType.group`. - .. attribute:: call - - The system message denoting call state, e.g. missed call, started call, - etc. - .. attribute:: channel_name_change - - The system message denoting that a channel's name has been changed. - .. attribute:: channel_icon_change - - The system message denoting that a channel's icon has been changed. - .. attribute:: pins_add - - The system message denoting that a pinned message has been added to a channel. - -.. class:: VoiceRegion - - Specifies the region a voice server belongs to. - - .. attribute:: us_west - - The US West region. - .. attribute:: us_east - - The US East region. - .. attribute:: us_central - - The US Central region. - .. attribute:: eu_west - - The EU West region. - .. attribute:: eu_central - - The EU Central region. - .. attribute:: singapore - - The Singapore region. - .. attribute:: london - - The London region. - .. attribute:: sydney - - The Sydney region. - .. attribute:: amsterdam - - The Amsterdam region. - .. attribute:: frankfurt - - The Frankfurt region. - - .. attribute:: brazil - - The Brazil region. - .. attribute:: vip_us_east - - The US East region for VIP guilds. - .. attribute:: vip_us_west - - The US West region for VIP guilds. - .. attribute:: vip_amsterdam - - The Amsterdam region for VIP guilds. - -.. class:: VerificationLevel - - Specifies a :class:`Guild`\'s verification level, which is the criteria in - which a member must meet before being able to send messages to the guild. - - .. container:: operations - - .. describe:: x == y - - Checks if two verification levels are equal. - .. describe:: x != y - - Checks if two verification levels are not equal. - .. describe:: x > y - - Checks if a verification level is higher than another. - .. describe:: x < y - - Checks if a verification level is lower than another. - .. describe:: x >= y - - Checks if a verification level is higher or equal to another. - .. describe:: x <= y - - Checks if a verification level is lower or equal to another. - - .. attribute:: none - - No criteria set. - .. attribute:: low - - Member must have a verified email on their Discord account. - .. attribute:: medium - - Member must have a verified email and be registered on Discord for more - than five minutes. - .. attribute:: high - - Member must have a verified email, be registered on Discord for more - than five minutes, and be a member of the guild itself for more than - ten minutes. - .. attribute:: table_flip - - An alias for :attr:`high`. - .. attribute:: extreme - - Member must have a verified phone on their Discord account. - - .. attribute:: double_table_flip - - An alias for :attr:`extreme`. - -.. class:: ContentFilter - - Specifies a :class:`Guild`\'s explicit content filter, which is the machine - learning algorithms that Discord uses to detect if an image contains - pornography or otherwise explicit content. - - .. container:: operations - - .. describe:: x == y - - Checks if two content filter levels are equal. - .. describe:: x != y - - Checks if two content filter levels are not equal. - .. describe:: x > y - - Checks if a content filter level is higher than another. - .. describe:: x < y - - Checks if a content filter level is lower than another. - .. describe:: x >= y - - Checks if a content filter level is higher or equal to another. - .. describe:: x <= y - - Checks if a content filter level is lower or equal to another. - - .. attribute:: disabled - - The guild does not have the content filter enabled. - .. attribute:: no_role - - The guild has the content filter enabled for members without a role. - .. attribute:: all_members - - The guild has the content filter enabled for every member. - -.. class:: Status - - Specifies a :class:`Member` 's status. - - .. attribute:: online - - The member is online. - .. attribute:: offline - - The member is offline. - .. attribute:: idle - - The member is idle. - .. attribute:: dnd - - The member is "Do Not Disturb". - .. attribute:: do_not_disturb - - An alias for :attr:`dnd`. - .. attribute:: invisible - - The member is "invisible". In reality, this is only used in sending - a presence a la :meth:`Client.change_presence`. When you receive a - user's presence this will be :attr:`offline` instead. - -.. class:: RelationshipType - - Specifies the type of :class:`Relationship` - - .. attribute:: friend - - You are friends with this user. - .. attribute:: blocked - - You have blocked this user. - .. attribute:: incoming_request - - The user has sent you a friend request. - .. attribute:: outgoing_request - - You have sent a friend request to this user. - - -.. class:: AuditLogAction - - Represents the type of action being done for a :class:`AuditLogEntry`\, - which is retrievable via :meth:`Guild.audit_logs`. - - .. attribute:: guild_update - - The guild has updated. Things that trigger this include: - - - Changing the guild vanity URL - - Changing the guild invite splash - - Changing the guild AFK channel or timeout - - Changing the guild voice server region - - Changing the guild icon - - Changing the guild moderation settings - - Changing things related to the guild widget - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Guild`. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.afk_channel` - - :attr:`~AuditLogDiff.system_channel` - - :attr:`~AuditLogDiff.afk_timeout` - - :attr:`~AuditLogDiff.default_message_notifications` - - :attr:`~AuditLogDiff.explicit_content_filter` - - :attr:`~AuditLogDiff.mfa_level` - - :attr:`~AuditLogDiff.name` - - :attr:`~AuditLogDiff.owner` - - :attr:`~AuditLogDiff.splash` - - :attr:`~AuditLogDiff.vanity_url_code` - - .. attribute:: channel_create - - A new channel was created. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - either a :class:`abc.GuildChannel` or :class:`Object` with an ID. - - A more filled out object in the :class:`Object` case can be found - by using :attr:`~AuditLogEntry.after`. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.name` - - :attr:`~AuditLogDiff.type` - - :attr:`~AuditLogDiff.overwrites` - - .. attribute:: channel_update - - A channel was updated. Things that trigger this include: - - - The channel name or topic was changed - - The channel bitrate was changed - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`abc.GuildChannel` or :class:`Object` with an ID. - - A more filled out object in the :class:`Object` case can be found - by using :attr:`~AuditLogEntry.after` or :attr:`~AuditLogEntry.before`. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.name` - - :attr:`~AuditLogDiff.type` - - :attr:`~AuditLogDiff.position` - - :attr:`~AuditLogDiff.overwrites` - - :attr:`~AuditLogDiff.topic` - - :attr:`~AuditLogDiff.bitrate` - - .. attribute:: channel_delete - - A channel was deleted. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - an :class:`Object` with an ID. - - A more filled out object can be found by using the - :attr:`~AuditLogEntry.before` object. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.name` - - :attr:`~AuditLogDiff.type` - - :attr:`~AuditLogDiff.overwrites` - - .. attribute:: overwrite_create - - A channel permission overwrite was created. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`abc.GuildChannel` or :class:`Object` with an ID. - - When this is the action, the type of :attr:`~AuditLogEntry.extra` is - either a :class:`Role` or :class:`Member`. If the object is not found - then it is a :class:`Object` with an ID being filled, a name, and a - ``type`` attribute set to either ``'role'`` or ``'member'`` to help - dictate what type of ID it is. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.deny` - - :attr:`~AuditLogDiff.allow` - - :attr:`~AuditLogDiff.id` - - :attr:`~AuditLogDiff.type` - - .. attribute:: overwrite_update - - A channel permission overwrite was changed, this is typically - when the permission values change. - - See :attr:`overwrite_create` for more information on how the - :attr:`~AuditLogEntry.target` and :attr:`~AuditLogEntry.extra` fields - are set. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.deny` - - :attr:`~AuditLogDiff.allow` - - :attr:`~AuditLogDiff.id` - - :attr:`~AuditLogDiff.type` - - .. attribute:: overwrite_delete - - A channel permission overwrite was deleted. - - See :attr:`overwrite_create` for more information on how the - :attr:`~AuditLogEntry.target` and :attr:`~AuditLogEntry.extra` fields - are set. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.deny` - - :attr:`~AuditLogDiff.allow` - - :attr:`~AuditLogDiff.id` - - :attr:`~AuditLogDiff.type` - - .. attribute:: kick - - A member was kicked. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`User` who got kicked. - - When this is the action, :attr:`~AuditLogEntry.changes` is empty. - - .. attribute:: member_prune - - A member prune was triggered. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - set to `None`. - - When this is the action, the type of :attr:`~AuditLogEntry.extra` is - set to an unspecified proxy object with two attributes: - - - ``delete_members_days``: An integer specifying how far the prune was. - - ``members_removed``: An integer specifying how many members were removed. - - When this is the action, :attr:`~AuditLogEntry.changes` is empty. - - .. attribute:: ban - - A member was banned. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`User` who got banned. - - When this is the action, :attr:`~AuditLogEntry.changes` is empty. - - .. attribute:: unban - - A member was unbanned. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`User` who got unbanned. - - When this is the action, :attr:`~AuditLogEntry.changes` is empty. - - .. attribute:: member_update - - A member has updated. This triggers in the following situations: - - - A nickname was changed - - They were server muted or deafened (or it was undo'd) - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Member` or :class:`User` who got updated. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.nick` - - :attr:`~AuditLogDiff.mute` - - :attr:`~AuditLogDiff.deaf` - - .. attribute:: member_role_update - - A member's role has been updated. This triggers when a member - either gains a role or losses a role. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Member` or :class:`User` who got the role. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.roles` - - .. attribute:: role_create - - A new role was created. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Role` or a :class:`Object` with the ID. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.colour` - - :attr:`~AuditLogDiff.mentionable` - - :attr:`~AuditLogDiff.hoist` - - :attr:`~AuditLogDiff.name` - - :attr:`~AuditLogDiff.permissions` - - .. attribute:: role_update - - A role was updated. This triggers in the following situations: - - - The name has changed - - The permissions have changed - - The colour has changed - - Its hoist/mentionable state has changed - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Role` or a :class:`Object` with the ID. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.colour` - - :attr:`~AuditLogDiff.mentionable` - - :attr:`~AuditLogDiff.hoist` - - :attr:`~AuditLogDiff.name` - - :attr:`~AuditLogDiff.permissions` - - .. attribute:: role_delete - - A role was deleted. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Role` or a :class:`Object` with the ID. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.colour` - - :attr:`~AuditLogDiff.mentionable` - - :attr:`~AuditLogDiff.hoist` - - :attr:`~AuditLogDiff.name` - - :attr:`~AuditLogDiff.permissions` - - .. attribute:: invite_create - - An invite was created. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Invite` that was created. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.max_age` - - :attr:`~AuditLogDiff.code` - - :attr:`~AuditLogDiff.temporary` - - :attr:`~AuditLogDiff.inviter` - - :attr:`~AuditLogDiff.channel` - - :attr:`~AuditLogDiff.uses` - - :attr:`~AuditLogDiff.max_uses` - - .. attribute:: invite_update - - An invite was updated. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Invite` that was updated. - - .. attribute:: invite_delete - - An invite was deleted. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Invite` that was deleted. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.max_age` - - :attr:`~AuditLogDiff.code` - - :attr:`~AuditLogDiff.temporary` - - :attr:`~AuditLogDiff.inviter` - - :attr:`~AuditLogDiff.channel` - - :attr:`~AuditLogDiff.uses` - - :attr:`~AuditLogDiff.max_uses` - - .. attribute:: webhook_create - - A webhook was created. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Object` with the webhook ID. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.channel` - - :attr:`~AuditLogDiff.name` - - :attr:`~AuditLogDiff.type` (always set to ``1`` if so) - - .. attribute:: webhook_update - - A webhook was updated. This trigger in the following situations: - - - The webhook name changed - - The webhook channel changed - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Object` with the webhook ID. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.channel` - - :attr:`~AuditLogDiff.name` - - .. attribute:: webhook_delete - - A webhook was deleted. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Object` with the webhook ID. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.channel` - - :attr:`~AuditLogDiff.name` - - :attr:`~AuditLogDiff.type` (always set to ``1`` if so) - - .. attribute:: emoji_create - - An emoji was created. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Emoji` or :class:`Object` with the emoji ID. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.name` - - .. attribute:: emoji_update - - An emoji was updated. This triggers when the name has changed. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Emoji` or :class:`Object` with the emoji ID. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.name` - - .. attribute:: emoji_delete - - An emoji was deleted. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Object` with the emoji ID. - - Possible attributes for :class:`AuditLogDiff`: - - - :attr:`~AuditLogDiff.name` - - .. attribute:: message_delete - - A message was deleted by a moderator. Note that this - only triggers if the message was deleted by either bulk delete - or deletion by someone other than the author. - - When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Member` or :class:`User` who had their message deleted. - - When this is the action, the type of :attr:`~AuditLogEntry.extra` is - set to an unspecified proxy object with two attributes: - - - ``count``: An integer specifying how many messages were deleted. - - ``channel``: A :class:`TextChannel` or :class:`Object` with the channel ID where the message got deleted. - - -.. class:: AuditLogActionCategory - - Represents the category that the :class:`AuditLogAction` belongs to. - - This can be retrieved via :attr:`AuditLogEntry.category`. - - .. attribute:: create - - The action is the creation of something. - - .. attribute:: delete - - The action is the deletion of something. - - .. attribute:: update - - The action is the update of something. - - - -Async Iterator ----------------- - -Some API functions return an "async iterator". An async iterator is something that is -capable of being used in an `async for `_ -statement. - -These async iterators can be used as follows in 3.5 or higher: :: - - async for elem in channel.history(): - # do stuff with elem here - -If you are using 3.4 however, you will have to use the more verbose way: :: - - iterator = channel.history() # or whatever returns an async iterator - while True: - try: - item = yield from iterator.next() - except discord.NoMoreItems: - break - - # do stuff with item here - -Certain utilities make working with async iterators easier, detailed below. - -.. class:: AsyncIterator - - Represents the "AsyncIterator" concept. Note that no such class exists, - it is purely abstract. - - .. container:: operations - - .. describe:: async for x in y - - Iterates over the contents of the async iterator. Note - that this is only available in Python 3.5 or higher. - - - .. comethod:: next() - - |coro| - - Advances the iterator by one, if possible. If no more items are found - then this raises :exc:`NoMoreItems`. - - .. comethod:: get(**attrs) - - |coro| - - Similar to :func:`utils.get` except run over the async iterator. - - Getting the last message by a user named 'Dave' or ``None``: :: - - msg = await channel.history().get(author__name='Dave') - - .. comethod:: find(predicate) - - |coro| - - Similar to :func:`utils.find` except run over the async iterator. - - Unlike :func:`utils.find`\, the predicate provided can be a - coroutine. - - Getting the last audit log with a reason or ``None``: :: - - def predicate(event): - return event.reason is not None - - event = await guild.audit_logs().find(predicate) - - :param predicate: The predicate to use. Can be a coroutine. - :return: The first element that returns ``True`` for the predicate or ``None``. - - .. comethod:: flatten() - - |coro| - - Flattens the async iterator into a ``list`` with all the elements. - - :return: A list of every element in the async iterator. - :rtype: list - - .. method:: map(func) - - This is similar to the built-in ``map`` function. Another - :class:`AsyncIterator` is returned that executes the function on - every element it is iterating over. This function can either be a - regular function or a coroutine. - - Creating a content iterator: :: - - def transform(message): - return message.content - - async for content in channel.history().map(transform): - message_length = len(content) - - :param func: The function to call on every element. Could be a coroutine. - :return: An async iterator. - - .. method:: filter(predicate) - - This is similar to the built-in ``filter`` function. Another - :class:`AsyncIterator` is returned that filters over the original - async iterator. This predicate can be a regular function or a coroutine. - - Getting messages by non-bot accounts: :: - - def predicate(message): - return not message.author.bot - - async for elem in channel.history().filter(predicate): - ... - - :param predicate: The predicate to call on every element. Could be a coroutine. - :return: An async iterator. - - -Audit Log Data ----------------- - -Working with :meth:`Guild.audit_logs` is a complicated process with a lot of machinery -involved. The library attempts to make it easy to use and friendly. In order to accomplish -this goal, it must make use of a couple of data classes that aid in this goal. - -.. autoclass:: AuditLogEntry - :members: - -.. class:: AuditLogChanges - - An audit log change set. - - .. attribute:: before - - The old value. The attribute has the type of :class:`AuditLogDiff`. - - Depending on the :class:`AuditLogActionCategory` retrieved by - :attr:`~AuditLogEntry.category`\, the data retrieved by this - attribute differs: - - +----------------------------------------+---------------------------------------------------+ - | Category | Description | - +----------------------------------------+---------------------------------------------------+ - | :attr:`~AuditLogActionCategory.create` | All attributes are set to ``None``. | - +----------------------------------------+---------------------------------------------------+ - | :attr:`~AuditLogActionCategory.delete` | All attributes are set the value before deletion. | - +----------------------------------------+---------------------------------------------------+ - | :attr:`~AuditLogActionCategory.update` | All attributes are set the value before updating. | - +----------------------------------------+---------------------------------------------------+ - | ``None`` | No attributes are set. | - +----------------------------------------+---------------------------------------------------+ - - .. attribute:: after - - The new value. The attribute has the type of :class:`AuditLogDiff`. - - Depending on the :class:`AuditLogActionCategory` retrieved by - :attr:`~AuditLogEntry.category`\, the data retrieved by this - attribute differs: - - +----------------------------------------+--------------------------------------------------+ - | Category | Description | - +----------------------------------------+--------------------------------------------------+ - | :attr:`~AuditLogActionCategory.create` | All attributes are set to the created value | - +----------------------------------------+--------------------------------------------------+ - | :attr:`~AuditLogActionCategory.delete` | All attributes are set to ``None`` | - +----------------------------------------+--------------------------------------------------+ - | :attr:`~AuditLogActionCategory.update` | All attributes are set the value after updating. | - +----------------------------------------+--------------------------------------------------+ - | ``None`` | No attributes are set. | - +----------------------------------------+--------------------------------------------------+ - -.. class:: AuditLogDiff - - Represents an audit log "change" object. A change object has dynamic - attributes that depend on the type of action being done. Certain actions - map to certain attributes being set. - - Note that accessing an attribute that does not match the specified action - will lead to an attribute error. - - To get a list of attributes that have been set, you can iterate over - them. To see a list of all possible attributes that could be set based - on the action being done, check the documentation for :class:`AuditLogAction`, - otherwise check the documentation below for all attributes that are possible. - - .. describe:: iter(diff) - - Return an iterator over (attribute, value) tuple of this diff. - - .. attribute:: name - - *str* – A name of something. - - .. attribute:: icon - - *str* – A guild's icon hash. See also :attr:`Guild.icon`. - - .. attribute:: splash - - *str* – The guild's invite splash hash. See also :attr:`Guild.splash`. - - .. attribute:: owner - - Union[:class:`Member`, :class:`User`] – The guild's owner. See also :attr:`Guild.owner` - - .. attribute:: region - - :class:`GuildRegion` – The guild's voice region. See also :attr:`Guild.region`. - - .. attribute:: afk_channel - - Union[:class:`VoiceChannel`, :class:`Object`] – The guild's AFK channel. - - If this could not be found, then it falls back to a :class:`Object` - with the ID being set. - - See :attr:`Guild.afk_channel`. - - .. attribute:: system_channel - - Union[:class:`TextChannel`, :class:`Object`] – The guild's system channel. - - If this could not be found, then it falls back to a :class:`Object` - with the ID being set. - - See :attr:`Guild.system_channel`. - - .. attribute:: afk_timeout - - *int* – The guild's AFK timeout. See :attr:`Guild.afk_timeout`. - - .. attribute:: mfa_level - - *int* - The guild's MFA level. See :attr:`Guild.mfa_level`. - - .. attribute:: widget_enabled - - *bool* – The guild's widget has been enabled or disabled. - - .. attribute:: widget_channel - - Union[:class:`TextChannel`, :class:`Object`] – The widget's channel. - - If this could not be found then it falls back to a :class:`Object` - with the ID being set. - - .. attribute:: verification_level - - :class:`VerificationLevel` – The guild's verification level. - - See also :attr:`Guild.verification_level`. - - .. attribute:: explicit_content_filter - - :class:`ContentFilter` – The guild's content filter. - - See also :attr:`Guild.explicit_content_filter`. - - .. attribute:: default_message_notifications - - *int* – The guild's default message notification setting. - - .. attribute:: vanity_url_code - - *str* – The guild's vanity URL. - - See also :meth:`Guild.vanity_invite` and :meth:`Guild.change_vanity_invite`. - - .. attribute:: position - - *int* – The position of a :class:`Role` or :class:`abc.GuildChannel`. - - .. attribute:: type - - *Union[int, str]* – The type of channel or channel permission overwrite. - - If the type is an ``int``, then it is a type of channel which can be either - ``0`` to indicate a text channel or ``1`` to indicate a voice channel. - - If the type is a ``str``, then it is a type of permission overwrite which - can be either ``'role'`` or ``'member'``. - - .. attribute:: topic - - *str* – The topic of a :class:`TextChannel`. - - See also :attr:`TextChannel.topic`. - - .. attribute:: bitrate - - *int* – The bitrate of a :class:`VoiceChannel`. - - See also :attr:`VoiceChannel.bitrate`. - - .. attribute:: overwrites - - List[Tuple[target, :class:`PermissionOverwrite`]] – A list of - permission overwrite tuples that represents a target and a - :class:`PermissionOverwrite` for said target. - - The first element is the object being targeted, which can either - be a :class:`Member` or :class:`User` or :class:`Role`. If this object - is not found then it is a :class:`Object` with an ID being filled and - a ``type`` attribute set to either ``'role'`` or ``'member'`` to help - decide what type of ID it is. - - .. attribute:: roles - - List[Union[:class:`Role`, :class:`Object`]] – A list of roles being added or removed - from a member. - - If a role is not found then it is a :class:`Object` with the ID and name being - filled in. - - .. attribute:: nick - - *Optional[str]* – The nickname of a member. - - See also :attr:`Member.nick` - - .. attribute:: deaf - - *bool* – Whether the member is being server deafened. - - See also :attr:`VoiceState.deaf`. - - .. attribute:: mute - - *bool* – Whether the member is being server muted. - - See also :attr:`VoiceState.mute`. - - .. attribute:: permissions - - :class:`Permissions` – The permissions of a role. - - See also :attr:`Role.permissions`. - - .. attribute:: colour - color - - :class:`Colour` – The colour of a role. - - See also :attr:`Role.colour` - - .. attribute:: hoist - - *bool* – Whether the role is being hoisted or not. - - See also :attr:`Role.hoist` - - .. attribute:: mentionable - - *bool* – Whether the role is mentionable or not. - - See also :attr:`Role.mentionable` - - .. attribute:: code - - *str* – The invite's code. - - See also :attr:`Invite.code` - - .. attribute:: channel - - Union[:class:`abc.GuildChannel`, :class:`Object`] – A guild channel. - - If the channel is not found then it is a :class:`Object` with the ID - being set. In some cases the channel name is also set. - - .. attribute:: inviter - - :class:`User` – The user who created the invite. - - See also :attr:`Invite.inviter`. - - .. attribute:: max_uses - - *int* – The invite's max uses. - - See also :attr:`Invite.max_uses`. - - .. attribute:: uses - - *int* – The invite's current uses. - - See also :attr:`Invite.uses`. - - .. attribute:: max_age - - *int* – The invite's max age in seconds. - - See also :attr:`Invite.max_age`. - - .. attribute:: temporary - - *bool* – If the invite is a temporary invite. - - See also :attr:`Invite.temporary`. - - .. attribute:: allow - deny - - :class:`Permissions` – The permissions being allowed or denied. - - .. attribute:: id - - *int* – The ID of the object being changed. - - .. attribute:: avatar - - *str* – The avatar hash of a member. - - See also :attr:`User.avatar`. - -.. this is currently missing the following keys: reason and application_id - I'm not sure how to about porting these - -Webhook Support ------------------- - -discord.py offers support for creating, editing, and executing webhooks through the :class:`Webhook` class. - -.. autoclass:: Webhook - :members: - -Adapters -~~~~~~~~~ - -Adapters allow you to change how the request should be handled. They all build on a single -interface, :meth:`WebhookAdapter.request`. - -.. autoclass:: WebhookAdapter - :members: - -.. autoclass:: AsyncWebhookAdapter - :members: - -.. autoclass:: RequestsWebhookAdapter - :members: - -.. _discord_api_abcs: - -Abstract Base Classes ------------------------ - -An abstract base class (also known as an ``abc``) is a class that models can inherit -to get their behaviour. The Python implementation of an `abc `_ is -slightly different in that you can register them at run-time. **Abstract base classes cannot be instantiated**. -They are mainly there for usage with ``isinstance`` and ``issubclass``\. - -This library has a module related to abstract base classes, some of which are actually from the ``abc`` standard -module, others which are not. - -.. autoclass:: discord.abc.Snowflake - :members: - -.. autoclass:: discord.abc.User - :members: - -.. autoclass:: discord.abc.PrivateChannel - :members: - -.. autoclass:: discord.abc.GuildChannel - :members: - -.. autoclass:: discord.abc.Messageable - :members: - :exclude-members: history, typing - - .. autocomethod:: discord.abc.Messageable.history - :async-for: - - .. autocomethod:: discord.abc.Messageable.typing - :async-with: - -.. autoclass:: discord.abc.Connectable - -.. _discord_api_models: - -Discord Models ---------------- - -Models are classes that are received from Discord and are not meant to be created by -the user of the library. - -.. danger:: - - The classes listed below are **not intended to be created by users** and are also - **read-only**. - - For example, this means that you should not make your own :class:`User` instances - nor should you modify the :class:`User` instance yourself. - - If you want to get one of these model classes instances they'd have to be through - the cache, and a common way of doing so is through the :func:`utils.find` function - or attributes of model classes that you receive from the events specified in the - :ref:`discord-api-events`. - -.. note:: - - Nearly all classes here have ``__slots__`` defined which means that it is - impossible to have dynamic attributes to the data classes. - - More information about ``__slots__`` can be found - `in the official python documentation `_. - - -ClientUser -~~~~~~~~~~~~ - -.. autoclass:: ClientUser() - :members: - :inherited-members: - -Relationship -~~~~~~~~~~~~~~ - -.. autoclass:: Relationship() - :members: - -User -~~~~~ - -.. autoclass:: User() - :members: - :inherited-members: - :exclude-members: history, typing - - .. autocomethod:: history - :async-for: - - .. autocomethod:: typing - :async-with: - -Attachment -~~~~~~~~~~~ - -.. autoclass:: Attachment() - :members: - -Message -~~~~~~~ - -.. autoclass:: Message() - :members: - -Reaction -~~~~~~~~~ - -.. autoclass:: Reaction() - :members: - :exclude-members: users - - .. autocomethod:: users - :async-for: - -CallMessage -~~~~~~~~~~~~ - -.. autoclass:: CallMessage() - :members: - -GroupCall -~~~~~~~~~~ - -.. autoclass:: GroupCall() - :members: - -Guild -~~~~~~ - -.. autoclass:: Guild() - :members: - :exclude-members: audit_logs - - .. autocomethod:: audit_logs - :async-for: - -Member -~~~~~~ - -.. autoclass:: Member() - :members: - :inherited-members: - :exclude-members: history, typing - - .. autocomethod:: history - :async-for: - - .. autocomethod:: typing - :async-with: - -VoiceState -~~~~~~~~~~~ - -.. autoclass:: VoiceState() - :members: - -Emoji -~~~~~ - -.. autoclass:: Emoji() - :members: - -PartialReactionEmoji -~~~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: PartialReactionEmoji() - :members: - -Role -~~~~~ - -.. autoclass:: Role() - :members: - -TextChannel -~~~~~~~~~~~~ - -.. autoclass:: TextChannel() - :members: - :inherited-members: - :exclude-members: history, typing - - .. autocomethod:: history - :async-for: - - .. autocomethod:: typing - :async-with: - -VoiceChannel -~~~~~~~~~~~~~ - -.. autoclass:: VoiceChannel() - :members: - :inherited-members: - -CategoryChannel -~~~~~~~~~~~~~~~~~ - -.. autoclass:: CategoryChannel() - :members: - :inherited-members: - -DMChannel -~~~~~~~~~ - -.. autoclass:: DMChannel() - :members: - :inherited-members: - :exclude-members: history, typing - - .. autocomethod:: history - :async-for: - - .. autocomethod:: typing - :async-with: - -GroupChannel -~~~~~~~~~~~~ - -.. autoclass:: GroupChannel() - :members: - :inherited-members: - :exclude-members: history, typing - - .. autocomethod:: history - :async-for: - - .. autocomethod:: typing - :async-with: - - -Invite -~~~~~~~ - -.. autoclass:: Invite() - :members: - -.. _discord_api_data: - -Data Classes --------------- - -Some classes are just there to be data containers, this lists them. - -Unlike :ref:`models ` you are allowed to create -these yourself, even if they can also be used to hold attributes. - -Nearly all classes here have ``__slots__`` defined which means that it is -impossible to have dynamic attributes to the data classes. - -The only exception to this rule is :class:`Object`, which is made with -dynamic attributes in mind. - -More information about ``__slots__`` can be found -`in the official python documentation `_. - - -Object -~~~~~~~ - -.. autoclass:: Object - :members: - -Embed -~~~~~~ - -.. autoclass:: Embed - :members: - -File -~~~~~ - -.. autoclass:: File - :members: - -Colour -~~~~~~ - -.. autoclass:: Colour - :members: - -Game -~~~~ - -.. autoclass:: Game - :members: - -Permissions -~~~~~~~~~~~~ - -.. autoclass:: Permissions - :members: - -PermissionOverwrite -~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: PermissionOverwrite - :members: - -Exceptions ------------- - -The following exceptions are thrown by the library. - -.. autoexception:: DiscordException - -.. autoexception:: ClientException - -.. autoexception:: LoginFailure - -.. autoexception:: NoMoreItems - -.. autoexception:: HTTPException - :members: - -.. autoexception:: Forbidden - -.. autoexception:: NotFound - -.. autoexception:: InvalidArgument - -.. autoexception:: GatewayNotFound - -.. autoexception:: ConnectionClosed - -.. autoexception:: discord.opus.OpusError - -.. autoexception:: discord.opus.OpusNotLoaded diff --git a/discord.py-rewrite/docs/conf.py b/discord.py-rewrite/docs/conf.py deleted file mode 100644 index f0af548..0000000 --- a/discord.py-rewrite/docs/conf.py +++ /dev/null @@ -1,314 +0,0 @@ -# -*- coding: utf-8 -*- -# -# discord.py documentation build configuration file, created by -# sphinx-quickstart on Fri Aug 21 05:43:30 2015. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os -import re - -on_rtd = os.getenv('READTHEDOCS') == 'True' - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) -sys.path.append(os.path.abspath('extensions')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.extlinks', - 'sphinxcontrib.asyncio', - 'details' -] - -if on_rtd: - extensions.append('sphinxcontrib.napoleon') -else: - extensions.append('sphinx.ext.napoleon') - -autodoc_member_order = 'bysource' - -extlinks = { - 'issue': ('https://github.com/Rapptz/discord.py/issues/%s', 'issue '), -} - -rst_prolog = """ -.. |coro| replace:: This function is a |corourl|_. -.. |maybecoro| replace:: This function *could be a* |corourl|_. -.. |corourl| replace:: *coroutine* -.. _corourl: https://docs.python.org/3/library/asyncio-task.html#coroutine -""" - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'discord.py' -copyright = u'2015-2017, Rapptz' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. - -version = '' -with open('../discord/__init__.py') as f: - version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) - -# The full version, including alpha/beta/rc tags. -release = version - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'friendly' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -html_experimental_html5_writer = True - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'basic' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = { -# } - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'discord.pydoc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ('index', 'discord.py.tex', u'discord.py Documentation', - u'Rapptz', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'discord.py', u'discord.py Documentation', - [u'Rapptz'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'discord.py', u'discord.py Documentation', - u'Rapptz', 'discord.py', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - -def setup(app): - app.add_javascript('custom.js') diff --git a/discord.py-rewrite/docs/discord.rst b/discord.py-rewrite/docs/discord.rst deleted file mode 100644 index ac159ae..0000000 --- a/discord.py-rewrite/docs/discord.rst +++ /dev/null @@ -1,97 +0,0 @@ -.. _discord-intro: - -Creating a Bot Account -======================== - -In order to work with the library and the Discord API in general, we must first create a Discord Bot account. - -Creating a Bot account is a pretty straightforward process. - -1. Make sure you're logged on to the `Discord website `_. -2. Navigate to the `application page `_ -3. Click on the "New App" button. - - .. image:: /images/discord_create_app_button.png - :alt: The new app button. - -4. Give the application a name and a description if wanted and click "Create App". - - - You can also put an avatar you want your bot to use, don't worry you can change this later. - - **Leave the Redirect URI(s) blank** unless are creating a service. - - .. image:: /images/discord_create_app_form.png - :alt: The new application form filled in. -5. Create a Bot User by clicking on the accompanying button and confirming it. - - .. image:: /images/discord_create_bot_user.png - :alt: The Create a Bot User button. -6. Make sure that **Public Bot** is ticked if you want others to invite your bot. - - - You should also make sure that **Require OAuth2 Code Grant** is unchecked unless you - are developing a service that needs it. If you're unsure, then **leave it unchecked**. - - .. image:: /images/discord_bot_user_options.png - :alt: How the Bot User options should look like for most people. - -7. Click to reveal the token. - - - **This is not the Client Secret** - - Look at the image above to see where the **Token** is. - - .. warning:: - - It should be worth noting that this token is essentially your bot's - password. You should **never** share this to someone else. In doing so, - someone can log in to your bot and do malicious things, such as leaving - servers, ban all members inside a server, or pinging everyone maliciously. - - The possibilities are endless, so **do not share this token.** - -And that's it. You now have a bot account and you can login with that token. - -.. _discord_invite_bot: - -Inviting Your Bot -------------------- - -So you've made a Bot User but it's not actually in any server. - -If you want to invite your bot you must create an invite URL for your bot. - -First, you must fetch the Client ID of the Bot. You can find this in the Bot's application page. - -.. image:: /images/discord_client_id.png - :alt: The Bot's Client ID. - -Copy paste that into the pre-formatted URL: - -.. code-block:: none - - https://discordapp.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&scope=bot&permissions=0 - -Replace ``YOUR_CLIENT_ID`` with the Client ID we got in the previous step. For example, -in the image above our client ID is 312777964700041216 so the resulting URL would be -https://discordapp.com/oauth2/authorize?client_id=312777964700041216&scope=bot&permissions=0 -(note that this bot has been deleted). - -Now you can click the link and invite your bot to any server you have "Manage Server" permissions on. - -Adding Permissions -~~~~~~~~~~~~~~~~~~~~ - -In the above URL, you might have noticed an interesting bit, the ``permissions=0`` fragment. - -Bot accounts can request specific permissions to be granted upon joining. When the bot joins -the guild, they will be granted a managed role that contains the permissions you requested. -If the permissions is 0, then no special role is created. - -This ``permissions`` value is calculated based on bit-wise arithmetic. Thankfully, people have -created a calculator that makes it easy to calculate the permissions necessary visually. - -- https://discordapi.com/permissions.html -- https://finitereality.github.io/permissions/ - -Feel free to use whichever is easier for you to grasp. - -If you want to generate this URL dynamically at run-time inside your bot and using the -:class:`discord.Permissions` interface, you can use :func:`discord.utils.oauth_url`. diff --git a/discord.py-rewrite/docs/ext/commands/api.rst b/discord.py-rewrite/docs/ext/commands/api.rst deleted file mode 100644 index 9fae235..0000000 --- a/discord.py-rewrite/docs/ext/commands/api.rst +++ /dev/null @@ -1,225 +0,0 @@ -.. currentmodule:: discord - -API Reference -=============== - -The following section outlines the API of discord.py's command extension module. - -.. _ext_commands_api_bot: - -Bot ----- - -.. autoclass:: discord.ext.commands.Bot - :members: - :inherited-members: - -.. autoclass:: discord.ext.commands.AutoShardedBot - :members: - -.. autofunction:: discord.ext.commands.when_mentioned - -.. autofunction:: discord.ext.commands.when_mentioned_or - -.. _ext_commands_api_events: - -Event Reference ------------------ - -These events function similar to :ref:`the regular events `, except they -are custom to the command extension module. - -.. function:: on_command_error(ctx, error) - - An error handler that is called when an error is raised - inside a command either through user input error, check - failure, or an error in your own code. - - A default one is provided (:meth:`.Bot.on_command_error`). - - :param ctx: The invocation context. - :type ctx: :class:`Context` - :param error: The error that was raised. - :type error: :class:`CommandError` derived - -.. function:: on_command(ctx) - - An event that is called when a command is found and is about to be invoked. - - This event is called regardless of whether the command itself succeeds via - error or completes. - - :param ctx: The invocation context. - :type ctx: :class:`Context` - -.. function:: on_command_completion(ctx) - - An event that is called when a command has completed its invocation. - - This event is called only if the command succeeded, i.e. all checks have - passed and the user input it correctly. - - :param ctx: The invocation context. - :type ctx: :class:`Context` - -.. _ext_commands_api_command: - -Command --------- - -.. autofunction:: discord.ext.commands.command - -.. autofunction:: discord.ext.commands.group - -.. autoclass:: discord.ext.commands.Command - :members: - -.. autoclass:: discord.ext.commands.Group - :members: - :inherited-members: - -.. autoclass:: discord.ext.commands.GroupMixin - :members: - -.. _ext_commands_api_formatters: - -Formatters ------------ - -.. autoclass:: discord.ext.commands.Paginator - :members: - -.. autoclass:: discord.ext.commands.HelpFormatter - :members: - -.. _ext_commands_api_checks: - -Checks -------- - -.. autofunction:: discord.ext.commands.check - -.. autofunction:: discord.ext.commands.has_role - -.. autofunction:: discord.ext.commands.has_permissions - -.. autofunction:: discord.ext.commands.has_any_role - -.. autofunction:: discord.ext.commands.bot_has_role - -.. autofunction:: discord.ext.commands.bot_has_permissions - -.. autofunction:: discord.ext.commands.bot_has_any_role - -.. autofunction:: discord.ext.commands.cooldown - -.. autofunction:: discord.ext.commands.guild_only - -.. autofunction:: discord.ext.commands.is_owner - -.. autofunction:: discord.ext.commands.is_nsfw - -.. _ext_commands_api_context: - -Context --------- - -.. autoclass:: discord.ext.commands.Context - :members: - :inherited-members: - :exclude-members: history, typing - - .. autocomethod:: discord.ext.commands.Context.history - :async-for: - - .. autocomethod:: discord.ext.commands.Context.typing - :async-with: - -.. _ext_commands_api_converters: - -Converters ------------- - -.. autoclass:: discord.ext.commands.Converter - :members: - -.. autoclass:: discord.ext.commands.MemberConverter - :members: - -.. autoclass:: discord.ext.commands.UserConverter - :members: - -.. autoclass:: discord.ext.commands.TextChannelConverter - :members: - -.. autoclass:: discord.ext.commands.VoiceChannelConverter - :members: - -.. autoclass:: discord.ext.commands.CategoryChannelConverter - :members: - -.. autoclass:: discord.ext.commands.InviteConverter - :members: - -.. autoclass:: discord.ext.commands.RoleConverter - :members: - -.. autoclass:: discord.ext.commands.GameConverter - :members: - -.. autoclass:: discord.ext.commands.ColourConverter - :members: - -.. autoclass:: discord.ext.commands.EmojiConverter - :members: - -.. autoclass:: discord.ext.commands.clean_content - :members: - -.. _ext_commands_api_errors: - -Errors -------- - -.. autoexception:: discord.ext.commands.CommandError - :members: - -.. autoexception:: discord.ext.commands.MissingRequiredArgument - :members: - -.. autoexception:: discord.ext.commands.BadArgument - :members: - -.. autoexception:: discord.ext.commands.NoPrivateMessage - :members: - -.. autoexception:: discord.ext.commands.CheckFailure - :members: - -.. autoexception:: discord.ext.commands.CommandNotFound - :members: - -.. autoexception:: discord.ext.commands.DisabledCommand - :members: - -.. autoexception:: discord.ext.commands.CommandInvokeError - :members: - -.. autoexception:: discord.ext.commands.TooManyArguments - :members: - -.. autoexception:: discord.ext.commands.UserInputError - :members: - -.. autoexception:: discord.ext.commands.CommandOnCooldown - :members: - -.. autoexception:: discord.ext.commands.NotOwner - :members: - -.. autoexception:: discord.ext.commands.MissingPermissions - :members: - -.. autoexception:: discord.ext.commands.BotMissingPermissions - :members: - diff --git a/discord.py-rewrite/docs/ext/commands/commands.rst b/discord.py-rewrite/docs/ext/commands/commands.rst deleted file mode 100644 index dc8d7e7..0000000 --- a/discord.py-rewrite/docs/ext/commands/commands.rst +++ /dev/null @@ -1,587 +0,0 @@ -.. currentmodule:: discord - -.. _ext_commands_commands: - -Commands -========== - -One of the most appealing aspect of the command extension is how easy it is to define commands and -how you can arbitrarily nest groups and commands to have a rich sub-command system. - -Commands are defined by attaching it to a regular Python function. The command is then invoked by the user using a similar -signature to the Python function. - -For example, in the given command definition: - -.. code-block:: python3 - - @bot.command() - async def foo(ctx, arg): - await ctx.send(arg) - -With the following prefix (``$``), it would be invoked by the user via: - -.. code-block:: none - - $foo abc - -A command must always have at least one parameter, ``ctx``, which is the :class:`.Context` as the first one. - -There are two ways of registering a command. The first one is by using :meth:`.Bot.command` decorator, -as seen in the example above. The second is using the :func:`~ext.commands.command` decorator followed by -:meth:`.Bot.add_command` on the instance. - -Essentially, these two are equivalent: :: - - from discord.ext import commands - - bot = commands.Bot(command_prefix='$') - - @bot.command() - async def test(ctx): - pass - - # or: - - @commands.command() - async def test(ctx): - pass - - bot.add_command(test) - -Since the :meth:`.Bot.command` decorator is shorter and easier to comprehend, it will be the one used throughout the -documentation here. - -Any parameter that is accepted by the :class:`.Command` constructor can be passed into the decorator. For example, to change -the name to something other than the function would be as simple as doing this: - -.. code-block:: python3 - - @bot.command(name='list') - async def _list(ctx, arg): - pass - -Parameters ------------- - -Since we define commands by making Python functions, we also define the argument passing behaviour by the function -parameters. - -Certain parameter types do different things in the user side and most forms of parameter types are supported. - -Positional -++++++++++++ - -The most basic form of parameter passing is the positional parameter. This is where we pass a parameter as-is: - -.. code-block:: python3 - - @bot.command() - async def test(ctx, arg): - await ctx.send(arg) - - -On the bot using side, you can provide positional arguments by just passing a regular string: - -.. image:: /images/commands/positional1.png - -To make use of a word with spaces in between, you should quote it: - -.. image:: /images/commands/positional2.png - -As a note of warning, if you omit the quotes, you will only get the first word: - -.. image:: /images/commands/positional3.png - -Since positional arguments are just regular Python arguments, you can have as many as you want: - -.. code-block:: python3 - - @bot.command() - async def test(ctx, arg1, arg2): - await ctx.send('You passed {} and {}'.format(arg1, arg2)) - -Variable -++++++++++ - -Sometimes you want users to pass in an undetermined number of parameters. The library supports this -similar to how variable list parameters are done in Python: - -.. code-block:: python3 - - @bot.command() - async def test(ctx, *args): - await ctx.send('{} arguments: {}'.format(len(args), ', '.join(args))) - -This allows our user to accept either one or many arguments as they please. This works similar to positional arguments, -so multi-word parameters should be quoted. - -For example, on the bot side: - -.. image:: /images/commands/variable1.png - -If the user wants to input a multi-word argument, they have to quote it like earlier: - -.. image:: /images/commands/variable2.png - -Do note that similar to the Python function behaviour, a user can technically pass no arguments -at all: - -.. image:: /images/commands/variable3.png - -Since the ``args`` variable is a `tuple `_, -you can do anything you would usually do with one. - -Keyword-Only Arguments -++++++++++++++++++++++++ - -When you want to handle parsing of the argument yourself or do not feel like you want to wrap multi-word user input into -quotes, you can ask the library to give you the rest as a single argument. We do this by using a **keyword-only argument**, -seen below: - -.. code-block:: python3 - - @bot.command() - async def test(ctx, *, arg): - await ctx.send(arg) - -.. warning:: - - You can only have one keyword-only argument due to parsing ambiguities. - -On the bot side, we do not need to quote input with spaces: - -.. image:: /images/commands/keyword1.png - -Do keep in mind that wrapping it in quotes leaves it as-is: - -.. image:: /images/commands/keyword2.png - -By default, the keyword-only arguments are stripped of white space to make it easier to work with. This behaviour can be -toggled by the :attr:`.Command.rest_is_raw` argument in the decorator. - -.. _ext_commands_context: - -Invocation Context -------------------- - -As seen earlier, every command must take at least a single parameter, called the :class:`~ext.commands.Context`. - -This parameter gives you access to something called the "invocation context". Essentially all the information you need to -know how the command was executed. It contains a lot of useful information: - -- :attr:`.Context.guild` to fetch the :class:`Guild` of the command, if any. -- :attr:`.Context.message` to fetch the :class:`Message` of the command. -- :attr:`.Context.author` to fetch the :class:`Member` or :class:`User` that called the command. -- :meth:`.Context.send` to send a message to the channel the command was used in. - -The context implements the :class:`abc.Messageable` interface, so anything you can do on a :class:`abc.Messageable` you -can do on the :class:`~ext.commands.Context`. - -Converters ------------- - -Adding bot arguments with function parameters is only the first step in defining your bot's command interface. To actually -make use of the arguments, we usually want to convert the data into a target type. We call these -:ref:`ext_commands_api_converters`. - -Converters come in a few flavours: - -- A regular callable object that takes an argument as a sole parameter and returns a different type. - - - These range from your own function, to something like ``bool`` or ``int``. - -- A custom class that inherits from :class:`~ext.commands.Converter`. - -Basic Converters -++++++++++++++++++ - -At its core, a basic converter is a callable that takes in an argument and turns it into something else. - -For example, if we wanted to add two numbers together, we could request that they are turned into integers -for us by specifying the converter: - -.. code-block:: python3 - - @bot.command() - async def add(ctx, a: int, b: int): - await ctx.send(a + b) - -We specify converters by using something called a **function annotation**. This is a Python 3 exclusive feature that was -introduced in :pep:`3107`. - -This works with any callable, such as a function that would convert a string to all upper-case: - -.. code-block:: python3 - - def to_upper(argument): - return argument.upper() - - @bot.command() - async def up(ctx, *, content: to_upper): - await ctx.send(content) - -.. _ext_commands_adv_converters: - -Advanced Converters -+++++++++++++++++++++ - -Sometimes a basic converter doesn't have enough information that we need. For example, sometimes we want to get some -information from the :class:`Message` that called the command or we want to do some asynchronous processing. - -For this, the library provides the :class:`~ext.commands.Converter` interface. This allows you to have access to the -:class:`.Context` and have the callable be asynchronous. Defining a custom converter using this interface requires -overriding a single method, :meth:`.Converter.convert`. - -An example converter: - -.. code-block:: python3 - - import random - - class Slapper(commands.Converter): - async def convert(self, ctx, argument): - to_slap = random.choice(ctx.guild.members) - return '{0.author} slapped {1} because *{2}*'.format(ctx, to_slap, argument) - - @bot.command() - async def slap(ctx, *, reason: Slapper): - await ctx.send(reason) - -The converter provided can either be constructed or not. Essentially these two are equivalent: - -.. code-block:: python3 - - @bot.command() - async def slap(ctx, *, reason: Slapper): - await ctx.send(reason) - - # is the same as... - - @bot.command() - async def slap(ctx, *, reason: Slapper()): - await ctx.send(reason) - -Having the possibility of the converter be constructed allows you to set up some state in the converter's ``__init__`` for -fine tuning the converter. An example of this is actually in the library, :class:`~ext.commands.clean_content`. - -.. code-block:: python3 - - @bot.command() - async def clean(ctx, *, content: commands.clean_content): - await ctx.send(content) - - # or for fine-tuning - - @bot.command() - async def clean(ctx, *, content: commands.clean_content(use_nicknames=False)): - await ctx.send(content) - - -If a converter fails to convert an argument to its designated target type, the :exc:`.BadArgument` exception must be -raised. - -Discord Converters -++++++++++++++++++++ - -Working with :ref:`discord_api_models` is a fairly common thing when defining commands, as a result the library makes -working with them easy. - -For example, to receive a :class:`Member`, you can just pass it as a converter: - -.. code-block:: python3 - - @bot.command() - async def joined(ctx, *, member: discord.Member): - await ctx.send('{0} joined on {0.joined_at}'.format(member)) - -When this command is executed, it attempts to convert the string given into a :class:`Member` and then passes it as a -parameter for the function. This works by checking if the string is a mention, an ID, a nickname, a username + discriminator, -or just a regular username. The default set of converters have been written to be as easy to use as possible. - -A lot of discord models work out of the gate as a parameter: - -- :class:`Member` -- :class:`User` -- :class:`TextChannel` -- :class:`VoiceChannel` -- :class:`CategoryChannel` -- :class:`Role` -- :class:`Invite` -- :class:`Game` -- :class:`Emoji` -- :class:`Colour` - -Having any of these set as the converter will intelligently convert the argument to the appropriate target type you -specify. - -Under the hood, these are implemented by the :ref:`ext_commands_adv_converters` interface. A table of the equivalent -converter is given below: - -+-----------------------+-------------------------------------------------+ -| Discord Class | Converter | -+-----------------------+-------------------------------------------------+ -| :class:`Member` | :class:`~ext.commands.MemberConverter` | -+-----------------------+-------------------------------------------------+ -| :class:`User` | :class:`~ext.commands.UserConverter` | -+-----------------------+-------------------------------------------------+ -| :class:`TextChannel` | :class:`~ext.commands.TextChannelConverter` | -+-----------------------+-------------------------------------------------+ -| :class:`VoiceChannel` | :class:`~ext.commands.VoiceChannelConverter` | -+-----------------------+-------------------------------------------------+ -| :class:`VoiceChannel` | :class:`~ext.commands.CategoryChannelConverter` | -+-----------------------+-------------------------------------------------+ -| :class:`Role` | :class:`~ext.commands.RoleConverter` | -+-----------------------+-------------------------------------------------+ -| :class:`Invite` | :class:`~ext.commands.InviteConverter` | -+-----------------------+-------------------------------------------------+ -| :class:`Game` | :class:`~ext.commands.GameConverter` | -+-----------------------+-------------------------------------------------+ -| :class:`Emoji` | :class:`~ext.commands.EmojiConverter` | -+-----------------------+-------------------------------------------------+ -| :class:`Colour` | :class:`~ext.commands.ColourConverter` | -+-----------------------+-------------------------------------------------+ - -By providing the converter it allows us to use them as building blocks for another converter: - -.. code-block:: python3 - - class MemberRoles(commands.MemberConverter): - async def convert(self, ctx, argument): - member = await super().convert(ctx, argument) - return member.roles - - @bot.command() - async def roles(ctx, *, member: MemberRoles): - """Tells you a member's roles.""" - await ctx.send('I see the following roles: ' + ', '.join(member)) - -Inline Advanced Converters -+++++++++++++++++++++++++++++ - -If we don't want to inherit from :class:`~ext.commands.Converter`, we can still provide a converter that has the -advanced functionalities of an advanced converter and save us from specifying two types. - -For example, a common idiom would be to have a class and a converter for that class: - -.. code-block:: python3 - - class JoinDistance: - def __init__(self, joined, created): - self.joined = joined - self.created = created - - @property - def delta(self): - return self.joined - self.created - - class JoinDistanceConverter(commands.MemberConverter): - async def convert(self, ctx, argument): - member = await super().convert(ctx, argument) - return JoinDistance(member.joined_at, member.created_at) - - @bot.command() - async def delta(ctx, *, member: JoinDistanceConverter): - is_new = member.delta.days < 100 - if is_new: - await ctx.send("Hey you're pretty new!") - else: - await ctx.send("Hm you're not so new.") - -This can get tedious, so an inline advanced converter is possible through a ``classmethod`` inside the type: - -.. code-block:: python3 - - class JoinDistance: - def __init__(self, joined, created): - self.joined = joined - self.created = created - - @classmethod - async def convert(cls, ctx, argument): - member = await commands.MemberConverter().convert(ctx, argument) - return cls(member.joined_at, member.created_at) - - @property - def delta(self): - return self.joined - self.created - - @bot.command() - async def delta(ctx, *, member: JoinDistance): - is_new = member.delta.days < 100 - if is_new: - await ctx.send("Hey you're pretty new!") - else: - await ctx.send("Hm you're not so new.") - -.. _ext_commands_error_handler: - -Error Handling ----------------- - -When our commands fail to either parse we will, by default, receive a noisy error in ``stderr`` of our console that tells us -that an error has happened and has been silently ignored. - -In order to handle our errors, we must use something called an error handler. There is a global error handler, called -:func:`on_command_error` which works like any other event in the :ref:`discord-api-events`. This global error handler is -called for every error reached. - -Most of the time however, we want to handle an error local to the command itself. Luckily, commands come with local error -handlers that allow us to do just that. First we decorate an error handler function with :meth:`.Command.error`: - -.. code-block:: python3 - - @bot.command() - async def info(ctx, *, member: discord.Member): - """Tells you some info about the member.""" - fmt = '{0} joined on {0.joined_at} and has {1} roles.' - await ctx.send(fmt.format(member, len(member.roles))) - - @info.error - async def info_error(ctx, error): - if isinstance(error, commands.BadArgument): - await ctx.send('I could not find that member...') - -The first parameter of the error handler is the :class:`.Context` while the second one is an exception that is derived from -:exc:`~ext.commands.CommandError`. A list of errors is found in the :ref:`ext_commands_api_errors` page of the documentation. - -Checks -------- - -There are cases when we don't want a user to use our commands. They don't have permissions to do so or maybe we blocked -them from using our bot earlier. The commands extension comes with full support for these things in a concept called a -:ref:`ext_commands_api_checks`. - -A check is a basic predicate that can take in a :class:`.Context` as its sole parameter. Within it, you have the following -options: - -- Return ``True`` to signal that the person can run the command. -- Return ``False`` to signal that the person cannot run the command. -- Raise a :exc:`~ext.commands.CommandError` derived exception to signal the person cannot run the command. - - - This allows you to have custom error messages for you to handle in the - :ref:`error handlers `. - -To register a check for a command, we would have two ways of doing so. The first is using the :meth:`~ext.commands.check` -decorator. For example: - -.. code-block:: python3 - - async def is_owner(ctx): - return ctx.author.id == 316026178463072268 - - @bot.command(name='eval') - @commands.check(is_owner) - async def _eval(ctx, *, code): - """A bad example of an eval command""" - await ctx.send(eval(code)) - -This would only evaluate the command if the function ``is_owner`` returns ``True``. Sometimes we re-use a check often and -want to split it into its own decorator. To do that we can just add another level of depth: - -.. code-block:: python3 - - def is_owner(): - async def predicate(ctx): - return ctx.author.id == 316026178463072268 - return commands.check(predicate) - - @bot.command(name='eval') - @is_owner() - async def _eval(ctx, *, code): - """A bad example of an eval command""" - await ctx.send(eval(code)) - - -Since an owner check is so common, the library provides it for you (:func:`~ext.commands.is_owner`): - -.. code-block:: python3 - - @bot.command(name='eval') - @commands.is_owner() - async def _eval(ctx, *, code): - """A bad example of an eval command""" - await ctx.send(eval(code)) - -When multiple checks are specified, **all** of them must be ``True``: - -.. code-block:: python3 - - def is_in_guild(guild_id): - async def predicate(ctx): - return ctx.guild and ctx.guild.id == guild_id - return commands.check(is_in_guild) - - @bot.command() - @is_in_guild(41771983423143937) - async def secretguilddata(ctx): - """super secret stuff""" - await ctx.send('secret stuff') - -If any of those checks fail in the example above, then the command will not be run. - -When an error happens, the error is propagated to the :ref:`error handlers `. If you do not -raise a custom :exc:`~ext.commands.CommandError` derived exception, then it will get wrapped up into a -:exc:`~ext.commands.CheckFailure` exception as so: - -.. code-block:: python3 - - @bot.command() - @is_in_guild(41771983423143937) - async def secretguilddata(ctx): - """super secret stuff""" - await ctx.send('secret stuff') - - @secretguilddata.error - async def secretguilddata_error(ctx, error): - if isinstance(error, commands.CheckFailure): - await ctx.send('nothing to see here comrade.') - -If you want a more robust error system, you can derive from the exception and raise it instead of returning ``False``: - -.. code-block:: python3 - - class NoPrivateMessages(commands.CheckFailure): - pass - - def guild_only(): - async def predicate(ctx): - if ctx.guild is None: - raise NoPrivateMessages('Hey no DMs!') - return True - return commands.check(predicate) - - @guild_only() - async def test(ctx): - await ctx.send('Hey this is not a DM! Nice.') - - @test.error - async def test_error(ctx, error): - if isinstance(error, NoPrivateMessages): - await ctx.send(error) - -.. note:: - - Since having a ``guild_only`` decorator is pretty common, it comes built-in via :func:`~ext.commands.guild_only`. - -Global Checks -++++++++++++++ - -Sometimes we want to apply a check to **every** command, not just certain commands. The library supports this as well -using the global check concept. - -Global checks work similarly to regular checks except they are registered with the :func:`.Bot.check` decorator. - -For example, to block all DMs we could do the following: - -.. code-block:: python3 - - @bot.check - async def globally_block_dms(ctx): - return ctx.guild is not None - -.. warning:: - - Be careful on how you write your global checks, as it could also lock you out of your own bot. - -.. need a note on global check once here I think diff --git a/discord.py-rewrite/docs/ext/commands/index.rst b/discord.py-rewrite/docs/ext/commands/index.rst deleted file mode 100644 index 559597c..0000000 --- a/discord.py-rewrite/docs/ext/commands/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -``discord.ext.commands`` -- Bot commands framework -==================================================== - -``discord.py`` offers a lower level aspect on interacting with Discord. Often times, the library is used for the creation of -bots. However this task can be daunting and confusing to get correctly the first time. Many times there comes a repetition in -creating a bot command framework that is extensible, flexible, and powerful. For this reason, ``discord.py`` comes with an -extension library that handles this for you. - - -.. toctree:: - :maxdepth: 2 - - commands - api diff --git a/discord.py-rewrite/docs/extensions/details.py b/discord.py-rewrite/docs/extensions/details.py deleted file mode 100644 index 96f39d5..0000000 --- a/discord.py-rewrite/docs/extensions/details.py +++ /dev/null @@ -1,55 +0,0 @@ -from docutils.parsers.rst import Directive -from docutils.parsers.rst import states, directives -from docutils.parsers.rst.roles import set_classes -from docutils import nodes - -class details(nodes.General, nodes.Element): - pass - -class summary(nodes.General, nodes.Element): - pass - -def visit_details_node(self, node): - self.body.append(self.starttag(node, 'details', CLASS=node.attributes.get('class', ''))) - -def visit_summary_node(self, node): - self.body.append(self.starttag(node, 'summary', CLASS=node.attributes.get('summary-class', ''))) - self.body.append(node.rawsource) - -def depart_details_node(self, node): - self.body.append('\n') - -def depart_summary_node(self, node): - self.body.append('') - -class DetailsDirective(Directive): - final_argument_whitespace = True - optional_arguments = 1 - - option_spec = { - 'class': directives.class_option, - 'summary-class': directives.class_option, - } - - has_content = True - - def run(self): - set_classes(self.options) - self.assert_has_content() - - text = '\n'.join(self.content) - node = details(text, **self.options) - - if self.arguments: - summary_node = summary(self.arguments[0], **self.options) - summary_node.source, summary_node.line = self.state_machine.get_source_and_line(self.lineno) - node += summary_node - - self.state.nested_parse(self.content, self.content_offset, node) - return [node] - -def setup(app): - app.add_node(details, html=(visit_details_node, depart_details_node)) - app.add_node(summary, html=(visit_summary_node, depart_summary_node)) - app.add_directive('details', DetailsDirective) - diff --git a/discord.py-rewrite/docs/faq.rst b/discord.py-rewrite/docs/faq.rst deleted file mode 100644 index bc01eab..0000000 --- a/discord.py-rewrite/docs/faq.rst +++ /dev/null @@ -1,333 +0,0 @@ -.. currentmodule:: discord -.. _faq: - -Frequently Asked Questions -=========================== - -This is a list of Frequently Asked Questions regarding using ``discord.py`` and its extension modules. Feel free to suggest a -new question or submit one via pull requests. - -.. contents:: Questions - :local: - -Coroutines ------------- - -Questions regarding coroutines and asyncio belong here. - -I get a SyntaxError around the word ``async``\! What should I do? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This ``SyntaxError`` happens because you're using a Python version lower than 3.5. Python 3.4 uses ``@asyncio.coroutine`` and -``yield from`` instead of ``async def`` and ``await``. - -Thus you must do the following instead: :: - - async def foo(): - await bar() - - # into - - @asyncio.coroutine - def foo(): - yield from bar() - -Don't forget to ``import asyncio`` on the top of your files. - -**It is heavily recommended that you update to Python 3.5 or higher as it simplifies asyncio massively.** - -What is a coroutine? -~~~~~~~~~~~~~~~~~~~~~~ - -A coroutine is a function that must be invoked with ``await`` or ``yield from``. When Python encounters an ``await`` it stops -the function's execution at that point and works on other things until it comes back to that point and finishes off its work. -This allows for your program to be doing multiple things at the same time without using threads or complicated -multiprocessing. - -**If you forget to await a coroutine then the coroutine will not run. Never forget to await a coroutine.** - -Where can I use ``await``\? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can only use ``await`` inside ``async def`` functions and nowhere else. - -What does "blocking" mean? -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In asynchronous programming a blocking call is essentially all the parts of the function that are not ``await``. Do not -despair however, because not all forms of blocking are bad! Using blocking calls is inevitable, but you must work to make -sure that you don't excessively block functions. Remember, if you block for too long then your bot will freeze since it has -not stopped the function's execution at that point to do other things. - -A common source of blocking for too long is something like ``time.sleep(n)``. Don't do that. Use ``asyncio.sleep(n)`` -instead. Similar to this example: :: - - # bad - time.sleep(10) - - # good - await asyncio.sleep(10) - -Another common source of blocking for too long is using HTTP requests with the famous module ``requests``. While ``requests`` -is an amazing module for non-asynchronous programming, it is not a good choice for ``asyncio`` because certain requests can -block the event loop too long. Instead, use the ``aiohttp`` library which is installed on the side with this library. - -Consider the following example: :: - - # bad - r = requests.get('http://random.cat/meow') - if r.status_code == 200: - js = r.json() - await channel.send(js['file']) - - # good - async with aiohttp.ClientSession() as session: - async with session.get('http://random.cat/meow') as r: - if r.status == 200: - js = await r.json() - await channel.send(js['file']) - -General ---------- - -General questions regarding library usage belong here. - -How do I set the "Playing" status? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There is a method for this under :class:`Client` called :meth:`Client.change_presence`. The relevant aspect of this is its -``game`` keyword argument which takes in a :class:`Game` object. Putting both of these pieces of info together, you get the -following: :: - - await client.change_presence(game=discord.Game(name='my game')) - -How do I send a message to a specific channel? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You must fetch the channel directly and then call the appropriate method. Example: :: - - channel = client.get_channel(12324234183172) - await channel.send('hello') - -How do I upload an image? -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To upload something to Discord you have to use the :class:`File` object. - -A :class:`File` accepts two parameters, the file-like object (or file path) and the filename -to pass to Discord when uploading. - -If you want to upload an image it's as simple as: :: - - await channel.send(file=discord.File('my_file.png')) - -If you have a file-like object you can do as follows: :: - - with open('my_file.png', 'rb') as fp: - await channel.send(file=discord.File(fp, 'new_filename.png')) - -To upload multiple files, you can use the ``files`` keyword argument instead of ``file``\: :: - - my_files = [ - discord.File('result.zip'), - discord.File('teaser_graph.png'), - ] - await channel.send(files=my_files) - -If you want to upload something from a URL, you will have to use an HTTP request using ``aiohttp`` -and then pass an ``io.BytesIO`` instance to :class:`File` like so: - -.. code-block:: python3 - - import io - import aiohttp - - async with aiohttp.ClientSession() as session: - async with session.get(my_url) as resp: - if resp.status != 200: - return await channel.send('Could not download file...') - data = io.BytesIO(await resp.read()) - await channel.send(file=discord.File(data, 'cool_image.png')) - - -How can I add a reaction to a message? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You use the :meth:`Message.add_reaction` method. - -If you want to use unicode emoji, you must pass a valid unicode code point in a string. In your code, you can write this in a few different ways: - -- ``'👍'`` -- ``'\U0001F44D'`` -- ``'\N{THUMBS UP SIGN}'`` - -Quick example: :: - - await message.add_reaction('\N{THUMBS UP SIGN}') - -In case you want to use emoji that come from a message, you already get their code points in the content without needing -to do anything special. You **cannot** send ``':thumbsup:'`` style shorthands. - -For custom emoji, you should pass an instance of :class:`Emoji`. You can also pass a ``'name:id'`` string, but if you -can use said emoji, you should be able to use :meth:`Client.get_emoji` to get an emoji via ID or use :func:`utils.find`/ -:func:`utils.get` on :attr:`Client.emojis` or :attr:`Guild.emojis` collections. - -Quick example: :: - - # if you have the ID already - emoji = client.get_emoji(310177266011340803) - await message.add_reaction(emoji) - - # no ID, do a lookup - emoji = discord.utils.get(guild.emojis, name='LUL') - if emoji: - await message.add_reaction(emoji) - -How do I pass a coroutine to the player's "after" function? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The library's music player launches on a separate thread, ergo it does not execute inside a coroutine. -This does not mean that it is not possible to call a coroutine in the ``after`` parameter. To do so you must pass a callable -that wraps up a couple of aspects. - -The first gotcha that you must be aware of is that calling a coroutine is not a thread-safe operation. Since we are -technically in another thread, we must take caution in calling thread-safe operations so things do not bug out. Luckily for -us, ``asyncio`` comes with a ``asyncio.run_coroutine_threadsafe`` -`function `_ that allows us to call -a coroutine from another thread. - -.. warning:: - - This function is only part of 3.5.1+ and 3.4.4+. If you are not using these Python versions then use - ``discord.compat.run_coroutine_threadsafe``. - -However, this function returns a ``concurrent.Future`` and to actually call it we have to fetch its result. Putting all of -this together we can do the following: :: - - def my_after(error): - coro = some_channel.send('Song is done!') - fut = asyncio.run_coroutine_threadsafe(coro, client.loop) - try: - fut.result() - except: - # an error happened sending the message - pass - - voice.play(discord.FFmpegPCMAudio(url), after=my_after) - -How do I run something in the background? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -`Check the background_task.py example. `_ - -How do I get a specific model? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There are multiple ways of doing this. If you have a specific model's ID then you can use -one of the following functions: - -- :meth:`Client.get_channel` -- :meth:`Client.get_guild` -- :meth:`Client.get_user` -- :meth:`Client.get_emoji` -- :meth:`Guild.get_member` -- :meth:`Guild.get_channel` - -The following use an HTTP request: - -- :meth:`abc.Messageable.get_message` -- :meth:`Client.get_user_info` - - -If the functions above do not help you, then use of :func:`utils.find` or :func:`utils.get` would serve some use in finding -specific models. - -Quick example: :: - - # find a guild by name - guild = discord.utils.get(client.guilds, name='My Server') - - # make sure to check if it's found - if guild is not None: - # find a channel by name - channel = discord.utils.get(guild.text_channels, name='cool-channel') - -Commands Extension -------------------- - -Questions regarding ``discord.ext.commands`` belong here. - -Is there any documentation for this? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Not at the moment. Writing documentation for stuff takes time. A lot of people get by reading the docstrings in the source -code. Others get by via asking questions in the `Discord server `_. Others look at the -source code of `other existing bots `_. - -There is a `basic example `_ showcasing some -functionality. - -**Documentation is being worked on, it will just take some time to polish it**. - -Why does ``on_message`` make my commands stop working? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Overriding the default provided ``on_message`` forbids any extra commands from running. To fix this, add a -``bot.process_commands(message)`` line at the end of your ``on_message``. For example: :: - - @bot.event - async def on_message(message): - # do some extra stuff here - - await bot.process_commands(message) - -Why do my arguments require quotes? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In a simple command defined as: :: - - @bot.command() - async def echo(ctx, message: str): - await ctx.send(message) - -Calling it via ``?echo a b c`` will only fetch the first argument and disregard the rest. To fix this you should either call -it via ``?echo "a b c"`` or change the signature to have "consume rest" behaviour. Example: :: - - @bot.command() - async def echo(ctx, *, message: str): - await ctx.send(message) - -This will allow you to use ``?echo a b c`` without needing the quotes. - -How do I get the original ``message``\? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The :class:`~ext.commands.Context` contains an attribute, :attr:`~.Context.message` to get the original -message. - -Example: :: - - @bot.command() - async def joined_at(ctx, member: discord.Member = None): - member = member or ctx.author - await ctx.send('{0} joined at {0.joined_at}'.format(member)) - -How do I make a subcommand? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Use the ``group`` decorator. This will transform the callback into a ``Group`` which will allow you to add commands into -the group operating as "subcommands". These groups can be arbitrarily nested as well. - -Example: :: - - @bot.group() - async def git(ctx): - if ctx.invoked_subcommand is None: - await ctx.send('Invalid git command passed...') - - @git.command() - async def push(ctx, remote: str, branch: str): - await ctx.send('Pushing to {} {}'.format(remote, branch)) - -This could then be used as ``?git push origin master``. - diff --git a/discord.py-rewrite/docs/index.rst b/discord.py-rewrite/docs/index.rst deleted file mode 100644 index ba8c221..0000000 --- a/discord.py-rewrite/docs/index.rst +++ /dev/null @@ -1,59 +0,0 @@ -.. discord.py documentation master file, created by - sphinx-quickstart on Fri Aug 21 05:43:30 2015. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to discord.py -=========================== - -.. image:: /images/snake.png - -discord.py is a modern, easy to use, feature-rich, and async ready API wrapper -for Discord. - -**Features:** - -- Modern Pythonic API using ``async``\/``await`` syntax -- Sane rate limit handling that prevents 429s -- Implements the entire Discord API -- Command extension to aid with bot creation -- Easy to use with an object oriented design -- Optimised for both speed and memory - -Documentation Contents ------------------------ - -.. toctree:: - :maxdepth: 2 - - intro - quickstart - migrating - logging - api - -Extensions ------------ - -.. toctree:: - :maxdepth: 3 - - ext/commands/index.rst - - -Additional Information ------------------------ - -.. toctree:: - :maxdepth: 2 - - discord - faq - whats_new - -If you still can't find what you're looking for, try in one of the following pages: - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/discord.py-rewrite/docs/intro.rst b/discord.py-rewrite/docs/intro.rst deleted file mode 100644 index 6a8a4d7..0000000 --- a/discord.py-rewrite/docs/intro.rst +++ /dev/null @@ -1,112 +0,0 @@ -.. currentmodule:: discord - -.. _intro: - -Introduction -============== - -This is the documentation for discord.py, a library for Python to aid -in creating applications that utilise the Discord API. - -Prerequisites ---------------- - -discord.py works with Python 3.4.2 or higher. Support for earlier versions of Python -is not provided. Python 2.7 or lower is not supported. Python 3.3 is not supported -due to one of the dependencies (``aiohttp``) not supporting Python 3.3. - - -.. _installing: - -Installing ------------ - -You can get the library directly from PyPI: :: - - python3 -m pip install -U discord.py - -If you are using Windows, then the following should be used instead: :: - - py -3 -m pip install -U discord.py - - -To get voice support, you should use ``discord.py[voice]`` instead of ``discord.py``, e.g. :: - - python3 -m pip install -U discord.py[voice] - -On Linux environments, installing voice requires getting the following dependencies: - -- libffi -- libnacl -- python3-dev - -For a debian-based system, the following command will help get those dependencies: - -.. code-block:: shell - - $ apt install libffi-dev libnacl-dev python3-dev - -Remember to check your permissions! - -Virtual Environments -~~~~~~~~~~~~~~~~~~~~~ - -Sometimes we don't want to pollute our system installs with a library or we want to maintain -different versions of a library than the currently system installed one. Or we don't have permissions to -install a library along side with the system installed ones. For this purpose, the standard library as -of 3.3 comes with a concept called "Virtual Environment" to help maintain these separate versions. - -A more in-depth tutorial is found on `the official documentation. `_ - -However, for the quick and dirty: - -1. Go to your project's working directory: - - .. code-block:: shell - - $ cd your-bot-source - $ python3 -m venv bot-env - -2. Activate the virtual environment: - - .. code-block:: shell - - $ source bot-env/bin/activate - - On Windows you activate it with: - - .. code-block:: shell - - $ bot-env\Scripts\activate.bat - -3. Use pip like usual: - - .. code-block:: shell - - $ pip install -U discord.py - -Congratulations. You now have a virtual environment all set up without messing with your system installation. - -Basic Concepts ---------------- - -discord.py revolves around the concept of :ref:`events `. -An event is something you listen to and then respond to. For example, when a message -happens, you will receive an event about it and you can then respond to it. - -A quick example to showcase how events work: - -.. code-block:: python3 - - import discord - - class MyClient(discord.Client): - async def on_ready(self): - print('Logged on as {0}!'.format(self.user)) - - async def on_message(self, message): - print('Message from {0.author}: {0.content}'.format(message)) - - client = MyClient() - client.run('my token goes here') - diff --git a/discord.py-rewrite/docs/logging.rst b/discord.py-rewrite/docs/logging.rst deleted file mode 100644 index 0395575..0000000 --- a/discord.py-rewrite/docs/logging.rst +++ /dev/null @@ -1,46 +0,0 @@ -.. versionadded:: 0.6.0 -.. _logging_setup: - -Setting Up Logging -=================== - -*discord.py* logs errors and debug information via the `logging`_ python -module. It is strongly recommended that the logging module is -configured, as no errors or warnings will be output if it is not set up. -Configuration of the ``logging`` module can be as simple as:: - - import logging - - logging.basicConfig(level=logging.INFO) - -Placed at the start of the application. This will output the logs from -discord as well as other libraries that uses the ``logging`` module -directly to the console. - -The optional ``level`` argument specifies what level of events to log -out and can any of ``CRITICAL``, ``ERROR``, ``WARNING``, ``INFO``, and -``DEBUG`` and if not specified defaults to ``WARNING``. - -More advance setups are possible with the ``logging`` module. To for -example write the logs to a file called ``discord.log`` instead of -outputting them to to the console the following snippet can be used:: - - import discord - import logging - - logger = logging.getLogger('discord') - logger.setLevel(logging.DEBUG) - handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w') - handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s')) - logger.addHandler(handler) - -This is recommended, especially at verbose levels such as ``INFO``, -and ``DEBUG`` as there are a lot of events logged and it would clog the -stdout of your program. - - - -For more information, check the documentation and tutorial of the -`logging`_ module. - -.. _logging: https://docs.python.org/2/library/logging.html diff --git a/discord.py-rewrite/docs/make.bat b/discord.py-rewrite/docs/make.bat deleted file mode 100644 index bb9b635..0000000 --- a/discord.py-rewrite/docs/make.bat +++ /dev/null @@ -1,263 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - echo. coverage to run coverage check of the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -REM Check if sphinx-build is available and fallback to Python version if any -%SPHINXBUILD% 2> nul -if errorlevel 9009 goto sphinx_python -goto sphinx_ok - -:sphinx_python - -set SPHINXBUILD=python -m sphinx.__init__ -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -:sphinx_ok - - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\discord.py.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\discord.py.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "coverage" ( - %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage - if errorlevel 1 exit /b 1 - echo. - echo.Testing of coverage in the sources finished, look at the ^ -results in %BUILDDIR%/coverage/python.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end diff --git a/discord.py-rewrite/docs/migrating.rst b/discord.py-rewrite/docs/migrating.rst deleted file mode 100644 index 57d4f3a..0000000 --- a/discord.py-rewrite/docs/migrating.rst +++ /dev/null @@ -1,1071 +0,0 @@ -.. currentmodule:: discord - -.. _migrating_1_0: - -Migrating to v1.0 -====================== - -v1.0 is one of the biggest breaking changes in the library due to a complete -redesign. - -The amount of changes are so massive and long that for all intents and purposes, it is a completely -new library. - -Part of the redesign involves making things more easy to use and natural. Things are done on the -:ref:`models ` instead of requiring a :class:`Client` instance to do any work. - -Major Model Changes ---------------------- - -Below are major model changes that have happened in v1.0 - -Snowflakes are int -~~~~~~~~~~~~~~~~~~~~ - -Before v1.0, all snowflakes (the ``id`` attribute) were strings. This has been changed to ``int``. - -Quick example: :: - - # before - ch = client.get_channel('84319995256905728') - if message.author.id == '80528701850124288': - ... - - # after - ch = client.get_channel(84319995256905728) - if message.author.id == 80528701850124288: - ... - -This change allows for fewer errors when using the Copy ID feature in the official client since you no longer have -to wrap it in quotes and allows for optimisation opportunities by allowing ETF to be used instead of JSON internally. - -Server is now Guild -~~~~~~~~~~~~~~~~~~~~~ - -The official API documentation calls the "Server" concept a "Guild" instead. In order to be more consistent with the -API documentation when necessary, the model has been renamed to :class:`Guild` and all instances referring to it has -been changed as well. - -A list of changes is as follows: - -+-------------------------------+----------------------------------+ -| Before | After | -+-------------------------------+----------------------------------+ -| ``Message.server`` | :attr:`Message.guild` | -+-------------------------------+----------------------------------+ -| ``Channel.server`` | :attr:`.GuildChannel.guild` | -+-------------------------------+----------------------------------+ -| ``Client.servers`` | :attr:`Client.guilds` | -+-------------------------------+----------------------------------+ -| ``Client.get_server`` | :meth:`Client.get_guild` | -+-------------------------------+----------------------------------+ -| ``Emoji.server`` | :attr:`Emoji.guild` | -+-------------------------------+----------------------------------+ -| ``Role.server`` | :attr:`Role.guild` | -+-------------------------------+----------------------------------+ -| ``Invite.server`` | :attr:`Invite.guild` | -+-------------------------------+----------------------------------+ -| ``Member.server`` | :attr:`Member.guild` | -+-------------------------------+----------------------------------+ -| ``Permissions.manage_server`` | :attr:`Permissions.manage_guild` | -+-------------------------------+----------------------------------+ -| ``VoiceClient.server`` | :attr:`VoiceClient.guild` | -+-------------------------------+----------------------------------+ -| ``Client.create_server`` | :meth:`Client.create_guild` | -+-------------------------------+----------------------------------+ - -.. _migrating_1_0_model_state: - -Models are Stateful -~~~~~~~~~~~~~~~~~~~~~ - -As mentioned earlier, a lot of functionality was moved out of :class:`Client` and -put into their respective :ref:`model `. - -A list of these changes is enumerated below. - -+---------------------------------------+------------------------------------------------------------------------------+ -| Before | After | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.add_reaction`` | :meth:`Message.add_reaction` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.add_roles`` | :meth:`Member.add_roles` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.ban`` | :meth:`Member.ban` or :meth:`Guild.ban` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.change_nickname`` | :meth:`Member.edit` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.clear_reactions`` | :meth:`Message.clear_reactions` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.create_channel`` | :meth:`Guild.create_text_channel` and :meth:`Guild.create_voice_channel` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.create_custom_emoji`` | :meth:`Guild.create_custom_emoji` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.create_invite`` | :meth:`abc.GuildChannel.create_invite` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.create_role`` | :meth:`Guild.create_role` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.delete_channel`` | :meth:`abc.GuildChannel.delete` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.delete_channel_permissions`` | :meth:`abc.GuildChannel.set_permissions` with ``overwrites`` set to ``None`` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.delete_custom_emoji`` | :meth:`Emoji.delete` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.delete_invite`` | :meth:`Invite.delete` or :meth:`Client.delete_invite` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.delete_message`` | :meth:`Message.delete` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.delete_messages`` | :meth:`TextChannel.delete_messages` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.delete_role`` | :meth:`Role.delete` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.delete_server`` | :meth:`Guild.delete` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.edit_channel`` | :meth:`TextChannel.edit` or :meth:`VoiceChannel.edit` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.edit_channel_permissions`` | :meth:`abc.GuildChannel.set_permissions` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.edit_custom_emoji`` | :meth:`Emoji.edit` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.edit_message`` | :meth:`Message.edit` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.edit_profile`` | :meth:`ClientUser.edit` (you get this from :attr:`Client.user`) | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.edit_role`` | :meth:`Role.edit` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.edit_server`` | :meth:`Guild.edit` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.estimate_pruned_members`` | :meth:`Guild.estimate_pruned_members` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.get_all_emojis`` | :attr:`Client.emojis` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.get_bans`` | :meth:`Guild.bans` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.get_message`` | :meth:`abc.Messageable.get_message` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.get_reaction_users`` | :meth:`Reaction.users` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.invites_from`` | :meth:`abc.GuildChannel.invites` or :meth:`Guild.invites` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.join_voice_channel`` | :meth:`VoiceChannel.connect` (see :ref:`migrating_1_0_voice`) | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.kick`` | :meth:`Guild.kick` or :meth:`Member.kick` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.leave_server`` | :meth:`Guild.leave` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.logs_from`` | :meth:`abc.Messageable.history` (see :ref:`migrating_1_0_async_iter`) | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.move_channel`` | :meth:`TextChannel.edit` or :meth:`VoiceChannel.edit` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.move_member`` | :meth:`Member.edit` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.move_role`` | :meth:`Role.edit` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.pin_message`` | :meth:`Message.pin` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.pins_from`` | :meth:`abc.Messageable.pins` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.prune_members`` | :meth:`Guild.prune_members` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.purge_from`` | :meth:`TextChannel.purge` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.remove_reaction`` | :meth:`Message.remove_reaction` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.remove_roles`` | :meth:`Member.remove_roles` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.replace_roles`` | :meth:`Member.edit` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.send_file`` | :meth:`abc.Messageable.send` (see :ref:`migrating_1_0_sending_messages`) | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.send_message`` | :meth:`abc.Messageable.send` (see :ref:`migrating_1_0_sending_messages`) | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.send_typing`` | :meth:`abc.Messageable.trigger_typing` (use :meth:`abc.Messageable.typing`) | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.server_voice_state`` | :meth:`Member.edit` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.start_private_message`` | :meth:`User.create_dm` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.unban`` | :meth:`Guild.unban` or :meth:`Member.unban` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.unpin_message`` | :meth:`Message.unpin` | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.wait_for_message`` | :meth:`Client.wait_for` (see :ref:`migrating_1_0_wait_for`) | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.wait_for_reaction`` | :meth:`Client.wait_for` (see :ref:`migrating_1_0_wait_for`) | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.wait_until_login`` | Removed | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.messages`` | Removed | -+---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.wait_until_ready`` | No change | -+---------------------------------------+------------------------------------------------------------------------------+ - -Property Changes -~~~~~~~~~~~~~~~~~~ - -In order to be a bit more consistent, certain things that were properties were changed to methods instead. - -The following are now methods instead of properties (requires parentheses): - -- :meth:`Role.is_default` -- :meth:`Client.is_ready` -- :meth:`Client.is_closed` - -Dict Value Change -~~~~~~~~~~~~~~~~~~~~~ - -Prior to v1.0 some aggregating properties that retrieved models would return "dict view" objects. - -As a consequence, when the dict would change size while you would iterate over it, a RuntimeError would -be raised and crash the task. To alleviate this, the "dict view" objects were changed into lists. - -The following views were changed to a list: - -- :attr:`Client.guilds` -- :attr:`Client.users` (new in v1.0) -- :attr:`Client.emojis` (new in v1.0) -- :attr:`Guild.channels` -- :attr:`Guild.text_channels` (new in v1.0) -- :attr:`Guild.voice_channels` (new in v1.0) -- :attr:`Guild.emojis` -- :attr:`Guild.members` - -Voice State Changes -~~~~~~~~~~~~~~~~~~~~~ - -Earlier, in v0.11.0 a :class:`VoiceState` class was added to refer to voice states along with a -:attr:`Member.voice` attribute to refer to it. - -However, it was transparent to the user. In an effort to make the library save more memory, the -voice state change is now more visible. - -The only way to access voice attributes is via the :attr:`Member.voice` attribute. Note that if -the member does not have a voice state this attribute can be ``None``. - -Quick example: :: - - # before - member.deaf - member.voice.voice_channel - - # after - if member.voice: # can be None - member.voice.deaf - member.voice.channel - - -User and Member Type Split -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In v1.0 to save memory, :class:`User` and :class:`Member` are no longer inherited. Instead, they are "flattened" -by having equivalent properties that map out to the functional underlying :class:`User`. Thus, there is no functional -change in how they are used. However this breaks ``isinstance`` checks and thus is something to keep in mind. - -These memory savings were accomplished by having a global :class:`User` cache, and as a positive consequence you -can now easily fetch a :class:`User` by their ID by using the new :meth:`Client.get_user`. You can also get a list -of all :class:`User` your client can see with :attr:`Client.users`. - -.. _migrating_1_0_channel_split: - -Channel Type Split -~~~~~~~~~~~~~~~~~~~~~ - -Prior to v1.0, channels were two different types, ``Channel`` and ``PrivateChannel`` with a ``is_private`` -property to help differentiate between them. - -In order to save memory the channels have been split into 4 different types: - -- :class:`TextChannel` for guild text channels. -- :class:`VoiceChannel` for guild voice channels. -- :class:`DMChannel` for DM channels with members. -- :class:`GroupChannel` for Group DM channels with members. - -With this split came the removal of the ``is_private`` attribute. You should now use ``isinstance``. - -The types are split into two different :ref:`discord_api_abcs`: - -- :class:`abc.GuildChannel` for guild channels. -- :class:`abc.PrivateChannel` for private channels (DMs and group DMs). - -So to check if something is a guild channel you would do: :: - - isinstance(channel, discord.abc.GuildChannel) - -And to check if it's a private channel you would do: :: - - isinstance(channel, discord.abc.PrivateChannel) - -Of course, if you're looking for only a specific type you can pass that too, e.g. :: - - isinstance(channel, discord.TextChannel) - -With this type split also came event changes, which are enumerated in :ref:`migrating_1_0_event_changes`. - - -Miscellaneous Model Changes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There were lots of other things added or removed in the models in general. - -They will be enumerated here. - -**Removed** - -- :meth:`Client.login` no longer accepts email and password logins. - - - Use a token and ``bot=False``. - -- ``Client.get_all_emojis`` - - - Use :attr:`Client.emojis` instead. - -- ``Client.wait_for_message`` and ``Client.wait_for_reaction`` are gone. - - - Use :meth:`Client.wait_for` instead. - -- ``Channel.voice_members`` - - - Use :attr:`VoiceChannel.members` instead. - -- ``Channel.is_private`` - - - Use ``isinstance`` instead with one of the :ref:`discord_api_abcs` instead. - - e.g. ``isinstance(channel, discord.abc.GuildChannel)`` will check if it isn't a private channel. - -- ``Client.accept_invite`` - - - There is no replacement for this one. This functionality is deprecated API wise. - -- ``Guild.default_channel`` / ``Server.default_channel`` and ``Channel.is_default`` - - - The concept of a default channel was removed from Discord. - See `#329 `_. - -- ``Message.edited_timestamp`` - - - Use :attr:`Message.edited_at` instead. - -- ``Message.timestamp`` - - - Use :attr:`Message.created_at` instead. - -- ``Colour.to_tuple()`` - - - Use :meth:`Colour.to_rgb` instead. - -- ``Permissions.view_audit_logs`` - - - Use :attr:`Permissions.view_audit_log` instead. - -**Changed** - -- :attr:`Member.avatar_url` and :attr:`User.avatar_url` now return the default avatar if a custom one is not set. -- :attr:`Message.embeds` is now a list of :class:`Embed` instead of ``dict`` objects. -- :attr:`Message.attachments` is now a list of :class:`Attachment` instead of ``dict`` object. - -**Added** - -- :class:`Attachment` to represent a discord attachment. -- :class:`CategoryChannel` to represent a channel category. -- :attr:`VoiceChannel.members` for fetching members connected to a voice channel. -- :attr:`TextChannel.members` for fetching members that can see the channel. -- :attr:`Role.members` for fetching members that have the role. -- :attr:`Guild.text_channels` for fetching text channels only. -- :attr:`Guild.voice_channels` for fetching voice channels only. -- :attr:`Guild.categories` for fetching channel categories only. -- :attr:`TextChannel.category` and :attr:`VoiceChannel.category` to get the category a channel belongs to. -- :meth:`Guild.by_category` to get channels grouped by their category. -- :attr:`Guild.chunked` to check member chunking status. -- :attr:`Guild.explicit_content_filter` to fetch the content filter. -- :attr:`Guild.shard_id` to get a guild's Shard ID if you're sharding. -- :attr:`Client.users` to get all visible :class:`User` instances. -- :meth:`Client.get_user` to get a :class:`User` by ID. -- :meth:`User.avatar_url_as` to get an avatar in a specific size or format. -- :meth:`Guild.vanity_invite` to fetch the guild's vanity invite. -- :meth:`Guild.audit_logs` to fetch the guild's audit logs. -- :attr:`Message.webhook_id` to fetch the message's webhook ID. -- :meth:`TextChannel.is_nsfw` to check if a text channel is NSFW. -- :meth:`Colour.from_rgb` to construct a :class:`Colour` from RGB tuple. - -.. _migrating_1_0_sending_messages: - -Sending Messages ------------------- - -One of the changes that were done was the merger of the previous ``Client.send_message`` and ``Client.send_file`` -functionality into a single method, :meth:`~abc.Messageable.send`. - -Basically: :: - - # before - await client.send_message(channel, 'Hello') - - # after - await channel.send('Hello') - -This supports everything that the old ``send_message`` supported such as embeds: :: - - e = discord.Embed(title='foo') - await channel.send('Hello', embed=e) - -There is a caveat with sending files however, as this functionality was expanded to support multiple -file attachments, you must now use a :class:`File` pseudo-namedtuple to upload a single file. :: - - # before - await client.send_file(channel, 'cool.png', filename='testing.png', content='Hello') - - # after - await channel.send('Hello', file=discord.File('cool.png', 'testing.png')) - -This change was to facilitate multiple file uploads: :: - - my_files = [ - discord.File('cool.png', 'testing.png'), - discord.File(some_fp, 'cool_filename.png'), - ] - - await channel.send('Your images:', files=my_files) - -.. _migrating_1_0_async_iter: - -Asynchronous Iterators ------------------------- - -Prior to v1.0, certain functions like ``Client.logs_from`` would return a different type if done in Python 3.4 or 3.5+. - -In v1.0, this change has been reverted and will now return a singular type meeting an abstract concept called -:class:`AsyncIterator`. - -This allows you to iterate over it like normal in Python 3.5+: :: - - async for message in channel.history(): - print(message) - -Or turn it into a list for either Python 3.4 or 3.5+: :: - - messages = await channel.history().flatten() # use yield from for 3.4! - for message in messages: - print(message) - -A handy aspect of returning :class:`AsyncIterator` is that it allows you to chain functions together such as -:meth:`AsyncIterator.map` or :meth:`AsyncIterator.filter`: :: - - async for m_id in channel.history().filter(lambda m: m.author == client.user).map(lambda m: m.id): - print(m_id) - -The functions passed to :meth:`AsyncIterator.map` or :meth:`AsyncIterator.filter` can be either coroutines or regular -functions. - -You can also get single elements a la :func:`discord.utils.find` or :func:`discord.utils.get` via -:meth:`AsyncIterator.get` or :meth:`AsyncIterator.find`: :: - - my_last_message = await channel.history().get(author=client.user) - -The following return :class:`AsyncIterator`: - -- :meth:`abc.Messageable.history` -- :meth:`Guild.audit_logs` -- :meth:`Reaction.users` - -.. _migrating_1_0_event_changes: - -Event Changes --------------- - -A lot of events have gone through some changes. - -Many events with ``server`` in the name were changed to use ``guild`` instead. - -Before: - -- ``on_server_join`` -- ``on_server_remove`` -- ``on_server_update`` -- ``on_server_role_create`` -- ``on_server_role_delete`` -- ``on_server_role_update`` -- ``on_server_emojis_update`` -- ``on_server_available`` -- ``on_server_unavailable`` - -After: - -- :func:`on_guild_join` -- :func:`on_guild_remove` -- :func:`on_guild_update` -- :func:`on_guild_role_create` -- :func:`on_guild_role_delete` -- :func:`on_guild_role_update` -- :func:`on_guild_emojis_update` -- :func:`on_guild_available` -- :func:`on_guild_unavailable` - - -The :func:`on_voice_state_update` event has received an argument change. - -Before: :: - - async def on_voice_state_update(before, after) - -After: :: - - async def on_voice_state_update(member, before, after) - -Instead of two :class:`Member` objects, the new event takes one :class:`Member` object and two :class:`VoiceState` objects. - -The :func:`on_guild_emojis_update` event has received an argument change. - -Before: :: - - async def on_guild_emojis_update(before, after) - -After: :: - - async def on_guild_emojis_update(guild, before, after) - -The first argument is now the :class:`Guild` that the emojis were updated from. - -The :func:`on_member_ban` event has received an argument change as well: - -Before: :: - - async def on_member_ban(member) - -After: :: - - async def on_member_ban(guild, user) - -As part of the change, the event can either receive a :class:`User` or :class:`Member`. To help in the cases that have -:class:`User`, the :class:`Guild` is provided as the first parameter. - -The ``on_channel_`` events have received a type level split (see :ref:`migrating_1_0_channel_split`). - -Before: - -- ``on_channel_delete`` -- ``on_channel_create`` -- ``on_channel_update`` - -After: - -- :func:`on_guild_channel_delete` -- :func:`on_guild_channel_create` -- :func:`on_guild_channel_update` -- :func:`on_private_channel_delete` -- :func:`on_private_channel_create` -- :func:`on_private_channel_update` - -The ``on_guild_channel_`` events correspond to :class:`abc.GuildChannel` being updated (i.e. :class:`TextChannel` -and :class:`VoiceChannel`) and the ``on_private_channel_`` events correspond to :class:`abc.PrivateChannel` being -updated (i.e. :class:`DMChannel` and :class:`GroupChannel`). - -.. _migrating_1_0_voice: - -Voice Changes ---------------- - -Voice sending has gone through a complete redesign. - -In particular: - -- Connection is done through :meth:`VoiceChannel.connect` instead of ``Client.join_voice_channel``. -- You no longer create players and operate on them (you no longer store them). -- You instead request :class:`VoiceClient` to play an :class:`AudioSource` via :meth:`VoiceClient.play`. -- There are different built-in :class:`AudioSource`\s. - - - :class:`FFmpegPCMAudio` is the equivalent of ``create_ffmpeg_player`` - -- create_ffmpeg_player/create_stream_player/create_ytdl_player have all been removed. - - - The goal is to create :class:`AudioSource` instead. - -- Using :meth:`VoiceClient.play` will not return an ``AudioPlayer``. - - - Instead, it's "flattened" like :class:`User` -> :class:`Member` is. - -- The ``after`` parameter now takes a single parameter (the error). - -Basically: - -Before: :: - - vc = await client.join_voice_channel(channel) - player = vc.create_ffmpeg_player('testing.mp3', after=lambda: print('done')) - player.start() - - player.is_playing() - player.pause() - player.resume() - player.stop() - # ... - -After: :: - - vc = await channel.connect() - vc.play(discord.FFmpegPCMAudio('testing.mp3'), after=lambda e: print('done', e)) - vc.is_playing() - vc.pause() - vc.resume() - vc.stop() - # ... - -With the changed :class:`AudioSource` design, you can now change the source that the :class:`VoiceClient` is -playing at runtime via :attr:`VoiceClient.source`. - -For example, you can add a :class:`PCMVolumeTransformer` to allow changing the volume: :: - - vc.source = discord.PCMVolumeTransformer(vc.source) - vc.source.volume = 0.6 - -An added benefit of the redesign is that it will be much more resilient towards reconnections: - -- The voice websocket will now automatically re-connect and re-do the handshake when disconnected. -- The initial connect handshake will now retry up to 5 times so you no longer get as many ``asyncio.TimeoutError``. -- Audio will now stop and resume when a disconnect is found. - - - This includes changing voice regions etc. - - -.. _migrating_1_0_wait_for: - -Waiting For Events --------------------- - -Prior to v1.0, the machinery for waiting for an event outside of the event itself was done through two different -functions, ``Client.wait_for_message`` and ``Client.wait_for_reaction``. One problem with one such approach is that it did -not allow you to wait for events outside of the ones provided by the library. - -In v1.0 the concept of waiting for another event has been generalised to work with any event as :meth:`Client.wait_for`. - -For example, to wait for a message: :: - - # before - msg = await client.wait_for_message(author=message.author, channel=message.channel) - - # after - def pred(m): - return m.author == message.author and m.channel == message.channel - - msg = await client.wait_for('message', check=pred) - -To facilitate multiple returns, :meth:`Client.wait_for` returns either a single argument, no arguments, or a tuple of -arguments. - -For example, to wait for a reaction: :: - - reaction, user = await client.wait_for('reaction_add', check=lambda r, u: u.id == 176995180300206080) - - # use user and reaction - -Since this function now can return multiple arguments, the ``timeout`` parameter will now raise a ``asyncio.TimeoutError`` -when reached instead of setting the return to ``None``. For example: - -.. code-block:: python3 - - def pred(m): - return m.author == message.author and m.channel == message.channel - - try: - - msg = await client.wait_for('message', check=pred, timeout=60.0) - except asyncio.TimeoutError: - await channel.send('You took too long...') - else: - await channel.send('You said {0.content}, {0.author}.'.format(msg)) - -Upgraded Dependencies ------------------------ - -Following v1.0 of the library, we've updated our requirements to ``aiohttp`` v2.0 or higher. - -Since this is a backwards incompatible change, it is recommended that you see the -`changes `_ and the -`migrating `_ pages for details on the breaking changes in -``aiohttp``. - -Of the most significant for common users is the removal of helper functions such as: - -- ``aiohttp.get`` -- ``aiohttp.post`` -- ``aiohttp.delete`` -- ``aiohttp.patch`` -- ``aiohttp.head`` -- ``aiohttp.put`` -- ``aiohttp.request`` - -It is recommended that you create a session instead: :: - - async with aiohttp.ClientSession() as sess: - async with sess.get('url') as resp: - # work with resp - -Since it is better to not create a session for every request, you should store it in a variable and then call -``session.close`` on it when it needs to be disposed. - -Sharding ----------- - -The library has received significant changes on how it handles sharding and now has sharding as a first-class citizen. - -If using a Bot account and you want to shard your bot in a single process then you can use the :class:`AutoShardedClient`. - -This class allows you to use sharding without having to launch multiple processes or deal with complicated IPC. - -It should be noted that **the sharded client does not support user accounts**. This is due to the changes in connection -logic and state handling. - -Usage is as simple as doing: :: - - client = discord.AutoShardedClient() - -instead of using :class:`Client`. - -This will launch as many shards as your bot needs using the ``/gateway/bot`` endpoint, which allocates about 1000 guilds -per shard. - -If you want more control over the sharding you can specify ``shard_count`` and ``shard_ids``. :: - - # launch 10 shards regardless - client = discord.AutoShardedClient(shard_count=10) - - # launch specific shard IDs in this process - client = discord.AutoShardedClient(shard_count=10, shard_ids=(1, 2, 5, 6)) - -For users of the command extension, there is also :class:`~ext.commands.AutoShardedBot` which behaves similarly. - -Connection Improvements -------------------------- - -In v1.0, the auto reconnection logic has been powered up significantly. - -:meth:`Client.connect` has gained a new keyword argument, ``reconnect`` that defaults to ``True`` which controls -the reconnect logic. When enabled, the client will automatically reconnect in all instances of your internet going -offline or Discord going offline with exponential back-off. - -:meth:`Client.run` and :meth:`Client.start` gains this keyword argument as well, but for most cases you will not -need to specify it unless turning it off. - -.. _migrating_1_0_commands: - -Command Extension Changes --------------------------- - -Due to the :ref:`migrating_1_0_model_state` changes, some of the design of the extension module had to -undergo some design changes as well. - -Context Changes -~~~~~~~~~~~~~~~~~ - -In v1.0, the :class:`.Context` has received a lot of changes with how it's retrieved and used. - -The biggest change is that ``pass_context=True`` no longer exists, :class:`.Context` is always passed. Ergo: - -.. code-block:: python3 - - # before - @bot.command() - async def foo(): - await bot.say('Hello') - - # after - @bot.command() - async def foo(ctx): - await ctx.send('Hello') - -The reason for this is because :class:`~ext.commands.Context` now meets the requirements of :class:`abc.Messageable`. This -makes it have similar functionality to :class:`TextChannel` or :class:`DMChannel`. Using :meth:`~.Context.send` -will either DM the user in a DM context or send a message in the channel it was in, similar to the old ``bot.say`` -functionality. The old helpers have been removed in favour of the new :class:`abc.Messageable` interface. See -:ref:`migrating_1_0_removed_helpers` for more information. - -Since the :class:`~ext.commands.Context` is now by default passed, several shortcuts have been added: - -**New Shortcuts** - -- :attr:`~ext.commands.Context.author` is a shortcut for ``ctx.message.author``. -- :attr:`~ext.commands.Context.guild` is a shortcut for ``ctx.message.guild``. -- :attr:`~ext.commands.Context.channel` is a shortcut for ``ctx.message.channel``. -- :attr:`~ext.commands.Context.me` is a shortcut for ``ctx.message.guild.me`` or ``ctx.bot.user``. -- :attr:`~ext.commands.Context.voice_client` is a shortcut for ``ctx.message.guild.voice_client``. - -**New Functionality** - -- :meth:`~.Context.reinvoke` to invoke a command again. - - - This is useful for bypassing cooldowns. - -Subclassing Context -++++++++++++++++++++ - -In v1.0, there is now the ability to subclass :class:`~ext.commands.Context` and use it instead of the default -provided one. - -For example, if you want to add some functionality to the context: - -.. code-block:: python3 - - class MyContext(commands.Context): - @property - def secret(self): - return 'my secret here' - -Then you can use :meth:`~ext.commands.Bot.get_context` inside :func:`on_message` with combination with -:meth:`~ext.commands.Bot.invoke` to use your custom context: - -.. code-block:: python3 - - class MyBot(commands.Bot): - async def on_message(self, message): - ctx = await self.get_context(message, cls=MyContext) - await self.invoke(ctx) - -Now inside your commands you will have access to your custom context: - -.. code-block:: python3 - - @bot.command() - async def secret(ctx): - await ctx.send(ctx.secret) - -.. _migrating_1_0_removed_helpers: - -Removed Helpers -+++++++++++++++++ - -With the new :class:`.Context` changes, a lot of message sending helpers have been removed. - -For a full list of changes, see below: - -+-----------------+------------------------------------------------------------+ -| Before | After | -+-----------------+------------------------------------------------------------+ -| ``Bot.say`` | :meth:`.Context.send` | -+-----------------+------------------------------------------------------------+ -| ``Bot.upload`` | :meth:`.Context.send` | -+-----------------+------------------------------------------------------------+ -| ``Bot.whisper`` | ``ctx.author.send`` | -+-----------------+------------------------------------------------------------+ -| ``Bot.type`` | :meth:`.Context.typing` or :meth:`.Context.trigger_typing` | -+-----------------+------------------------------------------------------------+ -| ``Bot.reply`` | No replacement. | -+-----------------+------------------------------------------------------------+ - -Command Changes -~~~~~~~~~~~~~~~~~ - -As mentioned earlier, the first command change is that ``pass_context=True`` no longer -exists, so there is no need to pass this as a parameter. - -Another change is the removal of ``no_pm=True``. Instead, use the new :func:`~ext.commands.guild_only` built-in -check. - -The ``commands`` attribute of :class:`~ext.commands.Bot` and :class:`~ext.commands.Group` have been changed from a -dictionary to a set that does not have aliases. To retrieve the previous dictionary behaviour, use ``all_commands`` instead. - -Command instances have gained new attributes and properties: - -1. :attr:`~ext.commands.Command.signature` to get the signature of the command. -2. :attr:`~.Command.usage`, an attribute to override the default signature. -3. :attr:`~.Command.root_parent` to get the root parent group of a subcommand. - -For :class:`~ext.commands.Group` and :class:`~ext.commands.Bot` the following changed: - -- Changed :attr:`~.GroupMixin.commands` to be a ``set`` without aliases. - - - Use :attr:`~.GroupMixin.all_commands` to get the old ``dict`` with all commands. - -Check Changes -~~~~~~~~~~~~~~~ - -Prior to v1.0, :func:`~ext.commands.check`\s could only be synchronous. As of v1.0 checks can now be coroutines. - -Along with this change, a couple new checks were added. - -- :func:`~ext.commands.guild_only` replaces the old ``no_pm=True`` functionality. -- :func:`~ext.commands.is_owner` uses the :meth:`Client.application_info` endpoint by default to fetch owner ID. - - - This is actually powered by a different function, :meth:`~ext.commands.Bot.is_owner`. - - You can set the owner ID yourself by setting :attr:`.Bot.owner_id`. - -- :func:`~ext.commands.is_nsfw` checks if the channel the command is in is a NSFW channel. - - - This is powered by the new :meth:`TextChannel.is_nsfw` method. - -Event Changes -~~~~~~~~~~~~~~~ - -All command extension events have changed. - -Before: :: - - on_command(command, ctx) - on_command_completion(command, ctx) - on_command_error(error, ctx) - -After: :: - - on_command(ctx) - on_command_completion(ctx) - on_command_error(ctx, error) - -The extraneous ``command`` parameter in :func:`.on_command` and :func:`.on_command_completion` -have been removed. The :class:`~ext.commands.Command` instance was not kept up-to date so it was incorrect. In order to get -the up to date :class:`~ext.commands.Command` instance, use the :attr:`.Context.command` -attribute. - -The error handlers, either :meth:`.Command.error` or :func:`.on_command_error`, -have been re-ordered to use the :class:`~ext.commands.Context` as its first parameter to be consistent with other events -and commands. - -Cog Changes -~~~~~~~~~~~~~ - -Cog special methods have changed slightly. - -The previous ``__check`` special method has been renamed to ``__global_check`` to make it more clear that it's a global -check. - -To complement the new ``__global_check`` there is now a new ``__local_check`` to facilitate a check that will run on -every command in the cog. There is also a ``__global_check_once``, which is similar to a global check instead it is only -called once per :meth:`.Bot.invoke` call rather than every :meth:`.Command.invoke` call. Practically, the difference is -only for black-listing users or channels without constantly opening a database connection. - -Cogs have also gained a ``__before_invoke`` and ``__after_invoke`` cog local before and after invocation hook, which -can be seen in :ref:`migrating_1_0_before_after_hook`. - -The final addition is cog-local error handler, ``__error``, that is run on every command in the cog. - -An example cog with every special method registered is as follows: :: - - class Cog: - def __unload(self): - print('cleanup goes here') - - def __global_check(self, ctx): - print('cog global check') - return True - - def __global_check_once(self, ctx): - print('cog global check once') - return True - - async def __local_check(self, ctx): - print('cog local check') - return await ctx.bot.is_owner(ctx.author) - - async def __error(self, ctx, error): - print('Error in {0.command.qualified_name}: {1}'.format(ctx, error)) - - async def __before_invoke(self, ctx): - print('cog local before: {0.command.qualified_name}'.format(ctx)) - - async def __after_invoke(self, ctx): - print('cog local after: {0.command.qualified_name}'.format(ctx)) - - -.. _migrating_1_0_before_after_hook: - -Before and After Invocation Hooks -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Commands have gained new before and after invocation hooks that allow you to do an action before and after a command is -run. - -They take a single parameter, :class:`~ext.commands.Context` and they must be a coroutine. - -They are on a global, per-cog, or per-command basis. - -Basically: :: - - - # global hooks: - - @bot.before_invoke - async def before_any_command(ctx): - # do something before a command is called - pass - - @bot.after_invoke - async def after_any_command(ctx): - # do something after a command is called - pass - -The after invocation is hook always called, **regardless of an error in the command**. This makes it ideal for some error -handling or clean up of certain resources such a database connection. - -The per-command registration is as follows: :: - - @bot.command() - async def foo(ctx): - await ctx.send('foo') - - @foo.before_invoke - async def before_foo_command(ctx): - # do something before the foo command is called - pass - - @foo.after_invoke - async def after_foo_command(ctx): - # do something after the foo command is called - pass - -The special cog method for these is ``__before_invoke`` and ``__after_invoke``, e.g.: :: - - class Cog: - async def __before_invoke(self, ctx): - ctx.secret_cog_data = 'foo' - - async def __after_invoke(self, ctx): - print('{0.command} is done...'.format(ctx)) - - @commands.command() - async def foo(self, ctx): - await ctx.send(ctx.secret_cog_data) - -To check if a command failed in the after invocation hook, you can use -:attr:`.Context.command_failed`. - -The invocation order is as follows: - -1. Command local before invocation hook -2. Cog local before invocation hook -3. Global before invocation hook -4. The actual command -5. Command local after invocation hook -6. Cog local after invocation hook -7. Global after invocation hook - -Converter Changes -~~~~~~~~~~~~~~~~~~~ - -Prior to v1.0, a converter was a type hint that could be a callable that could be invoked -with a singular argument denoting the argument passed by the user as a string. - -This system was eventually expanded to support a :class:`~ext.commands.Converter` system to -allow plugging in the :class:`~ext.commands.Context` and do more complicated conversions such -as the built-in "discord" converters. - -In v1.0 this converter system was revamped to allow instances of :class:`~ext.commands.Converter` derived -classes to be passed. For consistency, the :meth:`~ext.commands.Converter.convert` method was changed to -always be a coroutine and will now take the two arguments as parameters. - -Essentially, before: :: - - class MyConverter(commands.Converter): - def convert(self): - return self.ctx.message.server.me - -After: :: - - class MyConverter(commands.Converter): - async def convert(self, ctx, argument): - return ctx.me - -The command framework also got a couple new converters: - -- :class:`~ext.commands.clean_content` this is akin to :attr:`Message.clean_content` which scrubs mentions. -- :class:`~ext.commands.UserConverter` will now appropriately convert :class:`User` only. -- ``ChannelConverter`` is now split into two different converters. - - - :class:`~ext.commands.TextChannelConverter` for :class:`TextChannel`. - - :class:`~ext.commands.VoiceChannelConverter` for :class:`VoiceChannel`. diff --git a/discord.py-rewrite/docs/migrating_to_async.rst b/discord.py-rewrite/docs/migrating_to_async.rst deleted file mode 100644 index 109ad40..0000000 --- a/discord.py-rewrite/docs/migrating_to_async.rst +++ /dev/null @@ -1,322 +0,0 @@ -:orphan: - -.. currentmodule:: discord - -.. _migrating-to-async: - -Migrating to v0.10.0 -====================== - -v0.10.0 is one of the biggest breaking changes in the library due to massive -fundamental changes in how the library operates. - -The biggest major change is that the library has dropped support to all versions prior to -Python 3.4.2. This was made to support ``asyncio``, in which more detail can be seen -:issue:`in the corresponding issue <50>`. To reiterate this, the implication is that -**python version 2.7 and 3.3 are no longer supported**. - -Below are all the other major changes from v0.9.0 to v0.10.0. - -Event Registration --------------------- - -All events before were registered using :meth:`Client.event`. While this is still -possible, the events must be decorated with ``@asyncio.coroutine``. - -Before: - -.. code-block:: python3 - - @client.event - def on_message(message): - pass - -After: - -.. code-block:: python3 - - @client.event - @asyncio.coroutine - def on_message(message): - pass - -Or in Python 3.5+: - -.. code-block:: python3 - - @client.event - async def on_message(message): - pass - -Because there is a lot of typing, a utility decorator (:meth:`Client.async_event`) is provided -for easier registration. For example: - -.. code-block:: python3 - - @client.async_event - def on_message(message): - pass - - -Be aware however, that this is still a coroutine and your other functions that are coroutines must -be decorated with ``@asyncio.coroutine`` or be ``async def``. - -Event Changes --------------- - -Some events in v0.9.0 were considered pretty useless due to having no separate states. The main -events that were changed were the ``_update`` events since previously they had no context on what -was changed. - -Before: - -.. code-block:: python3 - - def on_channel_update(channel): pass - def on_member_update(member): pass - def on_status(member): pass - def on_server_role_update(role): pass - def on_voice_state_update(member): pass - def on_socket_raw_send(payload, is_binary): pass - - -After: - -.. code-block:: python3 - - def on_channel_update(before, after): pass - def on_member_update(before, after): pass - def on_server_role_update(before, after): pass - def on_voice_state_update(before, after): pass - def on_socket_raw_send(payload): pass - -Note that ``on_status`` was removed. If you want its functionality, use :func:`on_member_update`. -See :ref:`discord-api-events` for more information. Other removed events include ``on_socket_closed``, ``on_socket_receive``, and ``on_socket_opened``. - - -Coroutines ------------ - -The biggest change that the library went through is that almost every function in :class:`Client` -was changed to be a `coroutine `_. Functions -that are marked as a coroutine in the documentation must be awaited from or yielded from in order -for the computation to be done. For example... - -Before: - -.. code-block:: python3 - - client.send_message(message.channel, 'Hello') - -After: - -.. code-block:: python3 - - yield from client.send_message(message.channel, 'Hello') - - # or in python 3.5+ - await client.send_message(message.channel, 'Hello') - -In order for you to ``yield from`` or ``await`` a coroutine then your function must be decorated -with ``@asyncio.coroutine`` or ``async def``. - -Iterables ----------- - -For performance reasons, many of the internal data structures were changed into a dictionary to support faster -lookup. As a consequence, this meant that some lists that were exposed via the API have changed into iterables -and not sequences. In short, this means that certain attributes now only support iteration and not any of the -sequence functions. - -The affected attributes are as follows: - -- :attr:`Client.servers` -- :attr:`Client.private_channels` -- :attr:`Server.channels` -- :attr:`Server.members` - -Some examples of previously valid behaviour that is now invalid - -.. code-block:: python3 - - if client.servers[0].name == "test": - # do something - -Since they are no longer ``list``\s, they no longer support indexing or any operation other than iterating. -In order to get the old behaviour you should explicitly cast it to a list. - -.. code-block:: python3 - - servers = list(client.servers) - # work with servers - -.. warning:: - - Due to internal changes of the structure, the order you receive the data in - is not in a guaranteed order. - -Enumerations ------------- - -Due to dropping support for versions lower than Python 3.4.2, the library can now use -`enumerations `_ in places where it makes sense. - -The common places where this was changed was in the server region, member status, and channel type. - -Before: - -.. code-block:: python3 - - server.region == 'us-west' - member.status == 'online' - channel.type == 'text' - -After: - -.. code-block:: python3 - - server.region == discord.ServerRegion.us_west - member.status = discord.Status.online - channel.type == discord.ChannelType.text - -The main reason for this change was to reduce the use of finicky strings in the API as this -could give users a false sense of power. More information can be found in the :ref:`discord-api-enums` page. - -Properties ------------ - -A lot of function calls that returned constant values were changed into Python properties for ease of use -in format strings. - -The following functions were changed into properties: - -+----------------------------------------+--------------------------------------+ -| Before | After | -+----------------------------------------+--------------------------------------+ -| ``User.avatar_url()`` | :attr:`User.avatar_url` | -+----------------------------------------+--------------------------------------+ -| ``User.mention()`` | :attr:`User.mention` | -+----------------------------------------+--------------------------------------+ -| ``Channel.mention()`` | :attr:`Channel.mention` | -+----------------------------------------+--------------------------------------+ -| ``Channel.is_default_channel()`` | :attr:`Channel.is_default` | -+----------------------------------------+--------------------------------------+ -| ``Role.is_everyone()`` | :attr:`Role.is_everyone` | -+----------------------------------------+--------------------------------------+ -| ``Server.get_default_role()`` | :attr:`Server.default_role` | -+----------------------------------------+--------------------------------------+ -| ``Server.icon_url()`` | :attr:`Server.icon_url` | -+----------------------------------------+--------------------------------------+ -| ``Server.get_default_channel()`` | :attr:`Server.default_channel` | -+----------------------------------------+--------------------------------------+ -| ``Message.get_raw_mentions()`` | :attr:`Message.raw_mentions` | -+----------------------------------------+--------------------------------------+ -| ``Message.get_raw_channel_mentions()`` | :attr:`Message.raw_channel_mentions` | -+----------------------------------------+--------------------------------------+ - -Member Management -------------------- - -Functions that involved banning and kicking were changed. - -+--------------------------------+--------------------------+ -| Before | After | -+--------------------------------+--------------------------+ -| ``Client.ban(server, user)`` | ``Client.ban(member)`` | -+--------------------------------+--------------------------+ -| ``Client.kick(server, user)`` | ``Client.kick(member)`` | -+--------------------------------+--------------------------+ - -.. migrating-renames: - -Renamed Functions -------------------- - -Functions have been renamed. - -+------------------------------------+-------------------------------------------+ -| Before | After | -+------------------------------------+-------------------------------------------+ -| ``Client.set_channel_permissions`` | :meth:`Client.edit_channel_permissions` | -+------------------------------------+-------------------------------------------+ - -All the :class:`Permissions` related attributes have been renamed and the `can_` prefix has been -dropped. So for example, ``can_manage_messages`` has become ``manage_messages``. - -Forced Keyword Arguments -------------------------- - -Since 3.0+ of Python, we can now force questions to take in forced keyword arguments. A keyword argument is when you -explicitly specify the name of the variable and assign to it, for example: ``foo(name='test')``. Due to this support, -some functions in the library were changed to force things to take said keyword arguments. This is to reduce errors of -knowing the argument order and the issues that could arise from them. - -The following parameters are now exclusively keyword arguments: - -- :meth:`Client.send_message` - - ``tts`` -- :meth:`Client.logs_from` - - ``before`` - - ``after`` -- :meth:`Client.edit_channel_permissions` - - ``allow`` - - ``deny`` - -In the documentation you can tell if a function parameter is a forced keyword argument if it is after ``\*,`` -in the function signature. - -.. _migrating-running: - -Running the Client --------------------- - -In earlier versions of discord.py, ``client.run()`` was a blocking call to the main thread -that called it. In v0.10.0 it is still a blocking call but it handles the event loop for you. -However, in order to do that you must pass in your credentials to :meth:`Client.run`. - -Basically, before: - -.. code-block:: python3 - - client.login('token') - client.run() - -After: - -.. code-block:: python3 - - client.run('token') - -.. warning:: - - Like in the older ``Client.run`` function, the newer one must be the one of - the last functions to call. This is because the function is **blocking**. Registering - events or doing anything after :meth:`Client.run` will not execute until the function - returns. - -This is a utility function that abstracts the event loop for you. There's no need for -the run call to be blocking and out of your control. Indeed, if you want control of the -event loop then doing so is quite straightforward: - -.. code-block:: python3 - - import discord - import asyncio - - client = discord.Client() - - @asyncio.coroutine - def main_task(): - yield from client.login('token') - yield from client.connect() - - loop = asyncio.get_event_loop() - try: - loop.run_until_complete(main_task()) - except: - loop.run_until_complete(client.logout()) - finally: - loop.close() - - - diff --git a/discord.py-rewrite/docs/quickstart.rst b/discord.py-rewrite/docs/quickstart.rst deleted file mode 100644 index c1ffb85..0000000 --- a/discord.py-rewrite/docs/quickstart.rst +++ /dev/null @@ -1,76 +0,0 @@ -.. _quickstart: - -.. currentmodule:: discord - -Quickstart -============ - -This page gives a brief introduction to the library. It assumes you have the library installed, -if you don't check the :ref:`installing` portion. - -A Minimal Bot ---------------- - -Let's make a bot that replies to a specific message and walk you through it. - -It looks something like this: - -.. code-block:: python3 - - import discord - - client = discord.Client() - - @client.event - async def on_ready(): - print('We have logged in as {0.user}'.format(client)) - - @client.event - async def on_message(message): - if message.author == client.user: - return - - if message.content.startswith('$hello'): - await message.channel.send('Hello!') - - client.run('your token here') - -Let's name this file ``example_bot.py``. Make sure not to name it ``discord.py`` as that'll conflict -with the library. - -There's a lot going on here, so let's walk you through it step by step. - -1. The first line just imports the library, if this raises a `ModuleNotFoundError` or `ImportError` - then head on over to :ref:`installing` section to properly install. -2. Next, we create an instance of a :class:`Client`. This client is our connection to Discord. -3. We then use the :meth:`Client.event` decorator to register an event. This library has many events. - Since this library is asynchronous, we do things in a "callback" style manner. - - A callback is essentially a function that is called when something happens. In our case, - the :func:`on_ready` event is called when the bot has finished logging in and setting things - up and the :func:`on_message` event is called when the bot has received a message. -4. Since the :func:`on_message` event triggers for *every* message received, we have to make - sure that we ignore messages from ourselves. We do this by checking if the :attr:`Message.author` - is the same as the :attr:`Client.user`. -5. Afterwards, we check if the :class:`Message.content` starts with ``'$hello'``. If it is, - then we reply in the channel it was used in with ``'Hello!'``. -6. Finally, we run the bot with our login token. If you need help getting your token or creating a bot, - look in the :ref:`discord-intro` section. - - -Now that we've made a bot, we have to *run* the bot. Luckily, this is simple since this is just a -Python script, we can run it directly. - -On Windows: - -.. code-block:: shell - - $ py -3 example_bot.py - -On other systems: - -.. code-block:: shell - - $ python3 example_bot.py - -Now you can try playing around with your basic bot. diff --git a/discord.py-rewrite/docs/whats_new.rst b/discord.py-rewrite/docs/whats_new.rst deleted file mode 100644 index 069138a..0000000 --- a/discord.py-rewrite/docs/whats_new.rst +++ /dev/null @@ -1,364 +0,0 @@ -.. currentmodule:: discord - -.. _whats_new: - -Changelog -============ - -This page keeps a detailed human friendly rendering of what's new and changed -in specific versions. - -.. _vp0p16p6: - -v0.16.6 --------- - -Bug Fixes -~~~~~~~~~~ - -- Fix issue with :meth:`Client.create_server` that made it stop working. -- Fix main thread being blocked upon calling ``StreamPlayer.stop``. -- Handle HEARTBEAT_ACK and resume gracefully when it occurs. -- Fix race condition when pre-emptively rate limiting that caused releasing an already released lock. -- Fix invalid state errors when immediately cancelling a coroutine. - -.. _vp0p16p1: - -v0.16.1 --------- - -This release is just a bug fix release with some better rate limit implementation. - -Bug Fixes -~~~~~~~~~~~ - -- Servers are now properly chunked for user bots. -- The CDN URL is now used instead of the API URL for assets. -- Rate limit implementation now tries to use header information if possible. -- Event loop is now properly propagated (:issue:`420`) -- Allow falsey values in :meth:`Client.send_message` and :meth:`Client.send_file`. - -.. _vp0p16p0: - -v0.16.0 ---------- - -New Features -~~~~~~~~~~~~~~ - -- Add :attr:`Channel.overwrites` to get all the permission overwrites of a channel. -- Add :attr:`Server.features` to get information about partnered servers. - -Bug Fixes -~~~~~~~~~~ - -- Timeout when waiting for offline members while triggering :func:`on_ready`. - - - The fact that we did not timeout caused a gigantic memory leak in the library that caused - thousands of duplicate :class:`Member` instances causing big memory spikes. - -- Discard null sequences in the gateway. - - - The fact these were not discarded meant that :func:`on_ready` kept being called instead of - :func:`on_resumed`. Since this has been corrected, in most cases :func:`on_ready` will be - called once or twice with :func:`on_resumed` being called much more often. - -.. _vp0p15p1: - -v0.15.1 ---------- - -- Fix crash on duplicate or out of order reactions. - -.. _vp0p15p0: - -v0.15.0 --------- - -New Features -~~~~~~~~~~~~~~ - -- Rich Embeds for messages are now supported. - - - To do so, create your own :class:`Embed` and pass the instance to the ``embed`` keyword argument to :meth:`Client.send_message` or :meth:`Client.edit_message`. -- Add :meth:`Client.clear_reactions` to remove all reactions from a message. -- Add support for MESSAGE_REACTION_REMOVE_ALL event, under :func:`on_reaction_clear`. -- Add :meth:`Permissions.update` and :meth:`PermissionOverwrite.update` for bulk permission updates. - - - This allows you to use e.g. ``p.update(read_messages=True, send_messages=False)`` in a single line. -- Add :meth:`PermissionOverwrite.is_empty` to check if the overwrite is empty (i.e. has no overwrites set explicitly as true or false). - -For the command extension, the following changed: - -- ``Context`` is no longer slotted to facilitate setting dynamic attributes. - -.. _vp0p14p3: - -v0.14.3 ---------- - -Bug Fixes -~~~~~~~~~~~ - -- Fix crash when dealing with MESSAGE_REACTION_REMOVE -- Fix incorrect buckets for reactions. - -.. _v0p14p2: - -v0.14.2 ---------- - -New Features -~~~~~~~~~~~~~~ - -- :meth:`Client.wait_for_reaction` now returns a namedtuple with ``reaction`` and ``user`` attributes. - - This is for better support in the case that ``None`` is returned since tuple unpacking can lead to issues. - -Bug Fixes -~~~~~~~~~~ - -- Fix bug that disallowed ``None`` to be passed for ``emoji`` parameter in :meth:`Client.wait_for_reaction`. - -.. _v0p14p1: - -v0.14.1 ---------- - -Bug fixes -~~~~~~~~~~ - -- Fix bug with `Reaction` not being visible at import. - - This was also breaking the documentation. - -.. _v0p14p0: - -v0.14.0 --------- - -This update adds new API features and a couple of bug fixes. - -New Features -~~~~~~~~~~~~~ - -- Add support for Manage Webhooks permission under :attr:`Permissions.manage_webhooks` -- Add support for ``around`` argument in 3.5+ :meth:`Client.logs_from`. -- Add support for reactions. - - :meth:`Client.add_reaction` to add a reactions - - :meth:`Client.remove_reaction` to remove a reaction. - - :meth:`Client.get_reaction_users` to get the users that reacted to a message. - - :attr:`Permissions.add_reactions` permission bit support. - - Two new events, :func:`on_reaction_add` and :func:`on_reaction_remove`. - - :attr:`Message.reactions` to get reactions from a message. - - :meth:`Client.wait_for_reaction` to wait for a reaction from a user. - -Bug Fixes -~~~~~~~~~~ - -- Fix bug with Paginator still allowing lines that are too long. -- Fix the :attr:`Permissions.manage_emojis` bit being incorrect. - -.. _v0p13p0: - -v0.13.0 ---------- - -This is a backwards compatible update with new features. - -New Features -~~~~~~~~~~~~~ - -- Add the ability to manage emojis. - - - :meth:`Client.create_custom_emoji` to create new emoji. - - :meth:`Client.edit_custom_emoji` to edit an old emoji. - - :meth:`Client.delete_custom_emoji` to delete a custom emoji. -- Add new :attr:`Permissions.manage_emojis` toggle. - - - This applies for :class:`PermissionOverwrite` as well. -- Add new statuses for :class:`Status`. - - - :attr:`Status.dnd` (aliased with :attr:`Status.do_not_disturb`\) for Do Not Disturb. - - :attr:`Status.invisible` for setting your status to invisible (please see the docs for a caveat). -- Deprecate :meth:`Client.change_status` - - - Use :meth:`Client.change_presence` instead for better more up to date functionality. - - This method is subject for removal in a future API version. -- Add :meth:`Client.change_presence` for changing your status with the new Discord API change. - - - This is the only method that allows changing your status to invisible or do not disturb. - -Bug Fixes -~~~~~~~~~~ - -- Paginator pages do not exceed their max_size anymore (:issue:`340`) -- Do Not Disturb users no longer show up offline due to the new :class:`Status` changes. - -.. _v0p12p0: - -v0.12.0 ---------- - -This is a bug fix update that also comes with new features. - -New Features -~~~~~~~~~~~~~ - -- Add custom emoji support. - - - Adds a new class to represent a custom Emoji named :class:`Emoji` - - Adds a utility generator function, :meth:`Client.get_all_emojis`. - - Adds a list of emojis on a server, :attr:`Server.emojis`. - - Adds a new event, :func:`on_server_emojis_update`. -- Add new server regions to :class:`ServerRegion` - - - :attr:`ServerRegion.eu_central` and :attr:`ServerRegion.eu_west`. -- Add support for new pinned system message under :attr:`MessageType.pins_add`. -- Add order comparisons for :class:`Role` to allow it to be compared with regards to hierarchy. - - - This means that you can now do ``role_a > role_b`` etc to check if ``role_b`` is lower in the hierarchy. - -- Add :attr:`Server.role_hierarchy` to get the server's role hierarchy. -- Add :attr:`Member.server_permissions` to get a member's server permissions without their channel specific overwrites. -- Add :meth:`Client.get_user_info` to retrieve a user's info from their ID. -- Add a new ``Player`` property, ``Player.error`` to fetch the error that stopped the player. - - - To help with this change, a player's ``after`` function can now take a single parameter denoting the current player. -- Add support for server verification levels. - - - Adds a new enum called :class:`VerificationLevel`. - - This enum can be used in :meth:`Client.edit_server` under the ``verification_level`` keyword argument. - - Adds a new attribute in the server, :attr:`Server.verification_level`. -- Add :attr:`Server.voice_client` shortcut property for :meth:`Client.voice_client_in`. - - - This is technically old (was added in v0.10.0) but was undocumented until v0.12.0. - -For the command extension, the following are new: - -- Add custom emoji converter. -- All default converters that can take IDs can now convert via ID. -- Add coroutine support for ``Bot.command_prefix``. -- Add a method to reset command cooldown. - -Bug Fixes -~~~~~~~~~~ - -- Fix bug that caused the library to not work with the latest ``websockets`` library. -- Fix bug that leaked keep alive threads (:issue:`309`) -- Fix bug that disallowed :class:`ServerRegion` from being used in :meth:`Client.edit_server`. -- Fix bug in :meth:`Channel.permissions_for` that caused permission resolution to happen out of order. -- Fix bug in :attr:`Member.top_role` that did not account for same-position roles. - -.. _v0p11p0: - -v0.11.0 --------- - -This is a minor bug fix update that comes with a gateway update (v5 -> v6). - -Breaking Changes -~~~~~~~~~~~~~~~~~ - -- ``Permissions.change_nicknames`` has been renamed to :attr:`Permissions.change_nickname` to match the UI. - -New Features -~~~~~~~~~~~~~ - -- Add the ability to prune members via :meth:`Client.prune_members`. -- Switch the websocket gateway version to v6 from v5. This allows the library to work with group DMs and 1-on-1 calls. -- Add :attr:`AppInfo.owner` attribute. -- Add :class:`CallMessage` for group voice call messages. -- Add :class:`GroupCall` for group voice call information. -- Add :attr:`Message.system_content` to get the system message. -- Add the remaining VIP servers and the Brazil servers into :class:`ServerRegion` enum. -- Add ``stderr`` argument to :meth:`VoiceClient.create_ffmpeg_player` to redirect stderr. -- The library now handles implicit permission resolution in :meth:`Channel.permissions_for`. -- Add :attr:`Server.mfa_level` to query a server's 2FA requirement. -- Add :attr:`Permissions.external_emojis` permission. -- Add :attr:`Member.voice` attribute that refers to a :class:`VoiceState`. - - - For backwards compatibility, the member object will have properties mirroring the old behaviour. - -For the command extension, the following are new: - -- Command cooldown system with the ``cooldown`` decorator. -- ``UserInputError`` exception for the hierarchy for user input related errors. - -Bug Fixes -~~~~~~~~~~ - -- :attr:`Client.email` is now saved when using a token for user accounts. -- Fix issue when removing roles out of order. -- Fix bug where discriminators would not update. -- Handle cases where ``HEARTBEAT`` opcode is received. This caused bots to disconnect seemingly randomly. - -For the command extension, the following bug fixes apply: - -- ``Bot.check`` decorator is actually a decorator not requiring parentheses. -- ``Bot.remove_command`` and ``Group.remove_command`` no longer throw if the command doesn't exist. -- Command names are no longer forced to be ``lower()``. -- Fix a bug where Member and User converters failed to work in private message contexts. -- ``HelpFormatter`` now ignores hidden commands when deciding the maximum width. - -.. _v0p10p0: - -v0.10.0 -------- - -For breaking changes, see :ref:`migrating-to-async`. The breaking changes listed there will not be enumerated below. Since this version is rather a big departure from v0.9.2, this change log will be non-exhaustive. - -New Features -~~~~~~~~~~~~~ - -- The library is now fully ``asyncio`` compatible, allowing you to write non-blocking code a lot more easily. -- The library now fully handles 429s and unconditionally retries on 502s. -- A new command extension module was added but is currently undocumented. Figuring it out is left as an exercise to the reader. -- Two new exception types, :exc:`Forbidden` and :exc:`NotFound` to denote permission errors or 404 errors. -- Added :meth:`Client.delete_invite` to revoke invites. -- Added support for sending voice. Check :class:`VoiceClient` for more details. -- Added :meth:`Client.wait_for_message` coroutine to aid with follow up commands. -- Added :data:`version_info` named tuple to check version info of the library. -- Login credentials are now cached to have a faster login experience. You can disable this by passing in ``cache_auth=False`` - when constructing a :class:`Client`. -- New utility function, :func:`discord.utils.get` to simplify retrieval of items based on attributes. -- All data classes now support ``!=``, ``==``, ``hash(obj)`` and ``str(obj)``. -- Added :meth:`Client.get_bans` to get banned members from a server. -- Added :meth:`Client.invites_from` to get currently active invites in a server. -- Added :attr:`Server.me` attribute to get the :class:`Member` version of :attr:`Client.user`. -- Most data classes now support a ``hash(obj)`` function to allow you to use them in ``set`` or ``dict`` classes or subclasses. -- Add :meth:`Message.clean_content` to get a text version of the content with the user and channel mentioned changed into their names. -- Added a way to remove the messages of the user that just got banned in :meth:`Client.ban`. -- Added :meth:`Client.wait_until_ready` to facilitate easy creation of tasks that require the client cache to be ready. -- Added :meth:`Client.wait_until_login` to facilitate easy creation of tasks that require the client to be logged in. -- Add :class:`discord.Game` to represent any game with custom text to send to :meth:`Client.change_status`. -- Add :attr:`Message.nonce` attribute. -- Add :meth:`Member.permissions_in` as another way of doing :meth:`Channel.permissions_for`. -- Add :meth:`Client.move_member` to move a member to another voice channel. -- You can now create a server via :meth:`Client.create_server`. -- Added :meth:`Client.edit_server` to edit existing servers. -- Added :meth:`Client.server_voice_state` to server mute or server deafen a member. -- If you are being rate limited, the library will now handle it for you. -- Add :func:`on_member_ban` and :func:`on_member_unban` events that trigger when a member is banned/unbanned. - -Performance Improvements -~~~~~~~~~~~~~~~~~~~~~~~~~ - -- All data classes now use ``__slots__`` which greatly reduce the memory usage of things kept in cache. -- Due to the usage of ``asyncio``, the CPU usage of the library has gone down significantly. -- A lot of the internal cache lists were changed into dictionaries to change the ``O(n)`` lookup into ``O(1)``. -- Compressed READY is now on by default. This means if you're on a lot of servers (or maybe even a few) you would - receive performance improvements by having to download and process less data. -- While minor, change regex from ``\d+`` to ``[0-9]+`` to avoid unnecessary unicode character lookups. - -Bug Fixes -~~~~~~~~~~ - -- Fix bug where guilds being updated did not edit the items in cache. -- Fix bug where ``member.roles`` were empty upon joining instead of having the ``@everyone`` role. -- Fix bug where :meth:`Role.is_everyone` was not being set properly when the role was being edited. -- :meth:`Client.logs_from` now handles cases where limit > 100 to sidestep the discord API limitation. -- Fix bug where a role being deleted would trigger a ``ValueError``. -- Fix bug where :meth:`Permissions.kick_members` and :meth:`Permissions.ban_members` were flipped. -- Mentions are now triggered normally. This was changed due to the way discord handles it internally. -- Fix issue when a :class:`Message` would attempt to upgrade a :attr:`Message.server` when the channel is - a :class:`Object`. -- Unavailable servers were not being added into cache, this has been corrected. diff --git a/discord.py-rewrite/examples/background_task.py b/discord.py-rewrite/examples/background_task.py deleted file mode 100644 index a72862f..0000000 --- a/discord.py-rewrite/examples/background_task.py +++ /dev/null @@ -1,28 +0,0 @@ -import discord -import asyncio - -class MyClient(discord.Client): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # create the background task and run it in the background - self.bg_task = self.loop.create_task(self.my_background_task()) - - async def on_ready(self): - print('Logged in as') - print(self.user.name) - print(self.user.id) - print('------') - - async def my_background_task(self): - await self.wait_until_ready() - counter = 0 - channel = self.get_channel(1234567) # channel ID goes here - while not self.is_closed(): - counter += 1 - await channel.send(counter) - await asyncio.sleep(60) # task runs every 60 seconds - - -client = MyClient() -client.run('token') diff --git a/discord.py-rewrite/examples/basic_bot.py b/discord.py-rewrite/examples/basic_bot.py deleted file mode 100644 index 88909f4..0000000 --- a/discord.py-rewrite/examples/basic_bot.py +++ /dev/null @@ -1,65 +0,0 @@ -import discord -from discord.ext import commands -import random - -description = '''An example bot to showcase the discord.ext.commands extension -module. - -There are a number of utility commands being showcased here.''' -bot = commands.Bot(command_prefix='?', description=description) - -@bot.event -async def on_ready(): - print('Logged in as') - print(bot.user.name) - print(bot.user.id) - print('------') - -@bot.command() -async def add(ctx, left: int, right: int): - """Adds two numbers together.""" - await ctx.send(left + right) - -@bot.command() -async def roll(ctx, dice: str): - """Rolls a dice in NdN format.""" - try: - rolls, limit = map(int, dice.split('d')) - except Exception: - await ctx.send('Format has to be in NdN!') - return - - result = ', '.join(str(random.randint(1, limit)) for r in range(rolls)) - await ctx.send(result) - -@bot.command(description='For when you wanna settle the score some other way') -async def choose(ctx, *choices: str): - """Chooses between multiple choices.""" - await ctx.send(random.choice(choices)) - -@bot.command() -async def repeat(ctx, times: int, content='repeating...'): - """Repeats a message multiple times.""" - for i in range(times): - await ctx.send(content) - -@bot.command() -async def joined(ctx, member: discord.Member): - """Says when a member joined.""" - await ctx.send('{0.name} joined in {0.joined_at}'.format(member)) - -@bot.group() -async def cool(ctx): - """Says if a user is cool. - - In reality this just checks if a subcommand is being invoked. - """ - if ctx.invoked_subcommand is None: - await ctx.send('No, {0.subcommand_passed} is not cool'.format(ctx)) - -@cool.command(name='bot') -async def _bot(ctx): - """Is the bot cool?""" - await ctx.send('Yes, the bot is cool.') - -bot.run('token') diff --git a/discord.py-rewrite/examples/basic_voice.py b/discord.py-rewrite/examples/basic_voice.py deleted file mode 100644 index a689cc9..0000000 --- a/discord.py-rewrite/examples/basic_voice.py +++ /dev/null @@ -1,132 +0,0 @@ -import asyncio - -import discord -import youtube_dl - -from discord.ext import commands - -# Suppress noise about console usage from errors -youtube_dl.utils.bug_reports_message = lambda: '' - - -ytdl_format_options = { - 'format': 'bestaudio/best', - 'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s', - 'restrictfilenames': True, - 'noplaylist': True, - 'nocheckcertificate': True, - 'ignoreerrors': False, - 'logtostderr': False, - 'quiet': True, - 'no_warnings': True, - 'default_search': 'auto', - 'source_address': '0.0.0.0' # ipv6 addresses cause issues sometimes -} - -ffmpeg_options = { - 'before_options': '-nostdin', - 'options': '-vn' -} - -ytdl = youtube_dl.YoutubeDL(ytdl_format_options) - - -class YTDLSource(discord.PCMVolumeTransformer): - def __init__(self, source, *, data, volume=0.5): - super().__init__(source, volume) - - self.data = data - - self.title = data.get('title') - self.url = data.get('url') - - @classmethod - async def from_url(cls, url, *, loop=None): - loop = loop or asyncio.get_event_loop() - data = await loop.run_in_executor(None, ytdl.extract_info, url) - - if 'entries' in data: - # take first item from a playlist - data = data['entries'][0] - - filename = ytdl.prepare_filename(data) - return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data) - - -class Music: - def __init__(self, bot): - self.bot = bot - - @commands.command() - async def join(self, ctx, *, channel: discord.VoiceChannel): - """Joins a voice channel""" - - if ctx.voice_client is not None: - return await ctx.voice_client.move_to(channel) - - await channel.connect() - - @commands.command() - async def play(self, ctx, *, query): - """Plays a file from the local filesystem""" - - if ctx.voice_client is None: - if ctx.author.voice.channel: - await ctx.author.voice.channel.connect() - else: - return await ctx.send("Not connected to a voice channel.") - - if ctx.voice_client.is_playing(): - ctx.voice_client.stop() - - source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(query)) - ctx.voice_client.play(source, after=lambda e: print('Player error: %s' % e) if e else None) - - await ctx.send('Now playing: {}'.format(query)) - - @commands.command() - async def yt(self, ctx, *, url): - """Streams from a url (almost anything youtube_dl supports)""" - - if ctx.voice_client is None: - if ctx.author.voice.channel: - await ctx.author.voice.channel.connect() - else: - return await ctx.send("Not connected to a voice channel.") - - if ctx.voice_client.is_playing(): - ctx.voice_client.stop() - - player = await YTDLSource.from_url(url, loop=self.bot.loop) - ctx.voice_client.play(player, after=lambda e: print('Player error: %s' % e) if e else None) - - await ctx.send('Now playing: {}'.format(player.title)) - - @commands.command() - async def volume(self, ctx, volume: int): - """Changes the player's volume""" - - if ctx.voice_client is None: - return await ctx.send("Not connected to a voice channel.") - - ctx.voice_client.source.volume = volume - await ctx.send("Changed volume to {}%".format(volume)) - - - @commands.command() - async def stop(self, ctx): - """Stops and disconnects the bot from voice""" - - await ctx.voice_client.disconnect() - - -bot = commands.Bot(command_prefix=commands.when_mentioned_or("!"), - description='Music bot example') - -@bot.event -async def on_ready(): - print('Logged in as {0.id}/{0}'.format(bot.user)) - print('------') - -bot.add_cog(Music(bot)) -bot.run('token') diff --git a/discord.py-rewrite/examples/deleted.py b/discord.py-rewrite/examples/deleted.py deleted file mode 100644 index 6204c6f..0000000 --- a/discord.py-rewrite/examples/deleted.py +++ /dev/null @@ -1,21 +0,0 @@ -import discord - -class MyClient(discord.Client): - async def on_ready(self): - print('Connected!') - print('Username: {0.name}\nID: {0.id}'.format(self.user)) - - async def on_message(self, message): - if message.content.startswith('!deleteme'): - msg = await message.channel.send('I will delete myself now...') - await msg.delete() - - # this also works - await message.channel.send('Goodbye in 3 seconds...', delete_after=3.0) - - async def on_message_delete(self, message): - fmt = '{0.author} has deleted the message: {0.content}' - await message.channel.send(fmt.format(message)) - -client = MyClient() -client.run('token') diff --git a/discord.py-rewrite/examples/edits.py b/discord.py-rewrite/examples/edits.py deleted file mode 100644 index 2c1db4d..0000000 --- a/discord.py-rewrite/examples/edits.py +++ /dev/null @@ -1,20 +0,0 @@ -import discord -import asyncio - -class MyClient(discord.Client): - async def on_ready(self): - print('Connected!') - print('Username: {0.name}\nID: {0.id}'.format(self.user)) - - async def on_message(self, message): - if message.content.startswith('!editme'): - msg = await message.channel.send('10') - await asyncio.sleep(3.0) - await msg.edit(content='40') - - async def on_message_edit(self, before, after): - fmt = '**{0.author}** edited their message:\n{0.content} -> {1.content}' - await before.channel.send(fmt.format(before, after)) - -client = MyClient() -client.run('token') diff --git a/discord.py-rewrite/examples/guessing_game.py b/discord.py-rewrite/examples/guessing_game.py deleted file mode 100644 index a8f09c6..0000000 --- a/discord.py-rewrite/examples/guessing_game.py +++ /dev/null @@ -1,36 +0,0 @@ -import discord -import random -import asyncio - -class MyClient(discord.Client): - async def on_ready(self): - print('Logged in as') - print(self.user.name) - print(self.user.id) - print('------') - - async def on_message(self, message): - # we do not want the bot to reply to itself - if message.author.id == self.user.id: - return - - if message.content.startswith('$guess'): - await message.channel.send('Guess a number between 1 and 10.') - - def is_correct(m): - return m.author == message.author and m.content.isdigit() - - answer = random.randint(1, 10) - - try: - guess = await self.wait_for('message', check=is_correct, timeout=5.0) - except asyncio.TimeoutError: - return await message.channel.send('Sorry, you took too long it was {}.'.format(answer)) - - if int(guess.content) == answer: - await message.channel.send('You are right!') - else: - await message.channel.send('Oops. It is actually {}.'.format(answer)) - -client = MyClient() -client.run('token') diff --git a/discord.py-rewrite/examples/new_member.py b/discord.py-rewrite/examples/new_member.py deleted file mode 100644 index 67e700a..0000000 --- a/discord.py-rewrite/examples/new_member.py +++ /dev/null @@ -1,15 +0,0 @@ -import discord - -class MyClient(discord.Client): - async def on_ready(self): - print('Logged in as') - print(self.user.name) - print(self.user.id) - print('------') - - async def on_member_join(self, member): - guild = member.guild - await guild.default_channel.send('Welcome {0.mention} to {1.name}!'.format(member, guild)) - -client = MyClient() -client.run('token') diff --git a/discord.py-rewrite/examples/playlist.py b/discord.py-rewrite/examples/playlist.py deleted file mode 100644 index 7607b0c..0000000 --- a/discord.py-rewrite/examples/playlist.py +++ /dev/null @@ -1,246 +0,0 @@ -import asyncio -import discord -from discord.ext import commands - -if not discord.opus.is_loaded(): - # the 'opus' library here is opus.dll on windows - # or libopus.so on linux in the current directory - # you should replace this with the location the - # opus library is located in and with the proper filename. - # note that on windows this DLL is automatically provided for you - discord.opus.load_opus('opus') - -class VoiceEntry: - def __init__(self, message, player): - self.requester = message.author - self.channel = message.channel - self.player = player - - def __str__(self): - fmt = '*{0.title}* uploaded by {0.uploader} and requested by {1.display_name}' - duration = self.player.duration - if duration: - fmt = fmt + ' [length: {0[0]}m {0[1]}s]'.format(divmod(duration, 60)) - return fmt.format(self.player, self.requester) - -class VoiceState: - def __init__(self, bot): - self.current = None - self.voice = None - self.bot = bot - self.play_next_song = asyncio.Event() - self.songs = asyncio.Queue() - self.skip_votes = set() # a set of user_ids that voted - self.audio_player = self.bot.loop.create_task(self.audio_player_task()) - - def is_playing(self): - if self.voice is None or self.current is None: - return False - - player = self.current.player - return not player.is_done() - - @property - def player(self): - return self.current.player - - def skip(self): - self.skip_votes.clear() - if self.is_playing(): - self.player.stop() - - def toggle_next(self): - self.bot.loop.call_soon_threadsafe(self.play_next_song.set) - - async def audio_player_task(self): - while True: - self.play_next_song.clear() - self.current = await self.songs.get() - await self.bot.send_message(self.current.channel, 'Now playing ' + str(self.current)) - self.current.player.start() - await self.play_next_song.wait() - -class Music: - """Voice related commands. - - Works in multiple servers at once. - """ - def __init__(self, bot): - self.bot = bot - self.voice_states = {} - - def get_voice_state(self, server): - state = self.voice_states.get(server.id) - if state is None: - state = VoiceState(self.bot) - self.voice_states[server.id] = state - - return state - - async def create_voice_client(self, channel): - voice = await self.bot.join_voice_channel(channel) - state = self.get_voice_state(channel.server) - state.voice = voice - - def __unload(self): - for state in self.voice_states.values(): - try: - state.audio_player.cancel() - if state.voice: - self.bot.loop.create_task(state.voice.disconnect()) - except: - pass - - @commands.command(pass_context=True, no_pm=True) - async def join(self, ctx, *, channel : discord.Channel): - """Joins a voice channel.""" - try: - await self.create_voice_client(channel) - except discord.ClientException: - await self.bot.say('Already in a voice channel...') - except discord.InvalidArgument: - await self.bot.say('This is not a voice channel...') - else: - await self.bot.say('Ready to play audio in ' + channel.name) - - @commands.command(pass_context=True, no_pm=True) - async def summon(self, ctx): - """Summons the bot to join your voice channel.""" - summoned_channel = ctx.message.author.voice_channel - if summoned_channel is None: - await self.bot.say('You are not in a voice channel.') - return False - - state = self.get_voice_state(ctx.message.server) - if state.voice is None: - state.voice = await self.bot.join_voice_channel(summoned_channel) - else: - await state.voice.move_to(summoned_channel) - - return True - - @commands.command(pass_context=True, no_pm=True) - async def play(self, ctx, *, song : str): - """Plays a song. - - If there is a song currently in the queue, then it is - queued until the next song is done playing. - - This command automatically searches as well from YouTube. - The list of supported sites can be found here: - https://rg3.github.io/youtube-dl/supportedsites.html - """ - state = self.get_voice_state(ctx.message.server) - opts = { - 'default_search': 'auto', - 'quiet': True, - } - - if state.voice is None: - success = await ctx.invoke(self.summon) - if not success: - return - - try: - player = await state.voice.create_ytdl_player(song, ytdl_options=opts, after=state.toggle_next) - except Exception as e: - fmt = 'An error occurred while processing this request: ```py\n{}: {}\n```' - await self.bot.send_message(ctx.message.channel, fmt.format(type(e).__name__, e)) - else: - player.volume = 0.6 - entry = VoiceEntry(ctx.message, player) - await self.bot.say('Enqueued ' + str(entry)) - await state.songs.put(entry) - - @commands.command(pass_context=True, no_pm=True) - async def volume(self, ctx, value : int): - """Sets the volume of the currently playing song.""" - - state = self.get_voice_state(ctx.message.server) - if state.is_playing(): - player = state.player - player.volume = value / 100 - await self.bot.say('Set the volume to {:.0%}'.format(player.volume)) - - @commands.command(pass_context=True, no_pm=True) - async def pause(self, ctx): - """Pauses the currently played song.""" - state = self.get_voice_state(ctx.message.server) - if state.is_playing(): - player = state.player - player.pause() - - @commands.command(pass_context=True, no_pm=True) - async def resume(self, ctx): - """Resumes the currently played song.""" - state = self.get_voice_state(ctx.message.server) - if state.is_playing(): - player = state.player - player.resume() - - @commands.command(pass_context=True, no_pm=True) - async def stop(self, ctx): - """Stops playing audio and leaves the voice channel. - - This also clears the queue. - """ - server = ctx.message.server - state = self.get_voice_state(server) - - if state.is_playing(): - player = state.player - player.stop() - - try: - state.audio_player.cancel() - del self.voice_states[server.id] - await state.voice.disconnect() - except: - pass - - @commands.command(pass_context=True, no_pm=True) - async def skip(self, ctx): - """Vote to skip a song. The song requester can automatically skip. - - 3 skip votes are needed for the song to be skipped. - """ - - state = self.get_voice_state(ctx.message.server) - if not state.is_playing(): - await self.bot.say('Not playing any music right now...') - return - - voter = ctx.message.author - if voter == state.current.requester: - await self.bot.say('Requester requested skipping song...') - state.skip() - elif voter.id not in state.skip_votes: - state.skip_votes.add(voter.id) - total_votes = len(state.skip_votes) - if total_votes >= 3: - await self.bot.say('Skip vote passed, skipping song...') - state.skip() - else: - await self.bot.say('Skip vote added, currently at [{}/3]'.format(total_votes)) - else: - await self.bot.say('You have already voted to skip this song.') - - @commands.command(pass_context=True, no_pm=True) - async def playing(self, ctx): - """Shows info about the currently played song.""" - - state = self.get_voice_state(ctx.message.server) - if state.current is None: - await self.bot.say('Not playing anything.') - else: - skip_count = len(state.skip_votes) - await self.bot.say('Now playing {} [skips: {}/3]'.format(state.current, skip_count)) - -bot = commands.Bot(command_prefix=commands.when_mentioned_or('$'), description='A playlist example for discord.py') -bot.add_cog(Music(bot)) - -@bot.event -async def on_ready(): - print('Logged in as:\n{0} (ID: {0.id})'.format(bot.user)) - -bot.run('token') diff --git a/discord.py-rewrite/examples/reply.py b/discord.py-rewrite/examples/reply.py deleted file mode 100644 index c8f903f..0000000 --- a/discord.py-rewrite/examples/reply.py +++ /dev/null @@ -1,19 +0,0 @@ -import discord - -class MyClient(discord.Client): - async def on_ready(self): - print('Logged in as') - print(self.user.name) - print(self.user.id) - print('------') - - async def on_message(self, message): - # we do not want the bot to reply to itself - if message.author.id == self.user.id: - return - - if message.content.startswith('!hello'): - await message.channel.send('Hello {0.author.mention}'.format(message)) - -client = MyClient() -client.run('token') diff --git a/discord.py-rewrite/requirements.txt b/discord.py-rewrite/requirements.txt deleted file mode 100644 index 5a7e43e..0000000 --- a/discord.py-rewrite/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -aiohttp>=2.0.0,<2.3.0 -websockets>=3.1,<4.0 diff --git a/discord.py-rewrite/setup.py b/discord.py-rewrite/setup.py deleted file mode 100644 index b249214..0000000 --- a/discord.py-rewrite/setup.py +++ /dev/null @@ -1,68 +0,0 @@ -from setuptools import setup, find_packages -import re, os - -on_rtd = os.getenv('READTHEDOCS') == 'True' - -requirements = [] -with open('requirements.txt') as f: - requirements = f.read().splitlines() - -if on_rtd: - requirements.append('sphinxcontrib-napoleon') - requirements.append('sphinxcontrib-asyncio') - requirements.append('sphinx==1.6.3') - -version = '' -with open('discord/__init__.py') as f: - version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) - -if not version: - raise RuntimeError('version is not set') - -if version.endswith(('a', 'b', 'rc')): - # append version identifier based on commit count - try: - import subprocess - p = subprocess.Popen(['git', 'rev-list', '--count', 'HEAD'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = p.communicate() - if out: - version = version + out.decode('utf-8').strip() - except Exception: - pass - -readme = '' -with open('README.rst') as f: - readme = f.read() - -extras_require = { - 'voice': ['PyNaCl==1.1.2'], - 'docs': ['sphinxcontrib-asyncio'] -} - -setup(name='discord.py', - author='Rapptz', - url='https://github.com/Rapptz/discord.py', - version=version, - packages=['discord', 'discord.ext.commands'], - license='MIT', - description='A python wrapper for the Discord API', - long_description=readme, - include_package_data=True, - install_requires=requirements, - extras_require=extras_require, - classifiers=[ - 'Development Status :: 4 - Beta', - 'License :: OSI Approved :: MIT License', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Topic :: Internet', - 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Utilities', - ] -) diff --git a/porn/neko/download.py b/porn/neko/download.py deleted file mode 100644 index a1304d2..0000000 --- a/porn/neko/download.py +++ /dev/null @@ -1,58 +0,0 @@ - -import os -import json -import argparse -import praw -from time import sleep -from urllib.request import (Request, urlopen, urlretrieve) - - -def main(): - r = praw.Reddit( - user_agent='StroopwafelBot', - client_secret='Z4K17T8TC26cNAgDSRxCkE9jYR4', - client_id='aWeyKpzdHMtmJQ' - ) - r.read_only = True - - sub = r.subreddit('NekoIRL') - print(sub.random().url) -if __name__ == "__main__": - main() - -#def main(): -# -# parser = argparse.ArgumentParser() -# parser.add_argument('--sub', '-s') -# args = parser.parse_args() -# sub = args.sub -# imgs_folder = 'imgs_' + sub -# imgs_url = 'https://www.reddit.com/r/' + sub + '/top.json' -# #imgs_url = 'https://www.reddit.com/r/' + sub + '/about/stylesheet.json' -# req = Request(imgs_url) -# req.add_header('User-agent', 'Stylesheet images downloader Py3 v1') -# imgs_json = json.loads(urlopen(req).read()) -# imgs = [i for i in imgs_json['data']['children']] -# -# def fetch_images(total, count): -# if not os.path.exists(imgs_folder): -# os.makedirs(imgs_folder) -# os.chdir(imgs_folder) -# -# for i in imgs: -# url = i['url'] -# ext = '.' + url.split('.')[-1] -# name = i['name'] + ext -# urlretrieve(url, name) -# print('Downloading ' + str(count) + ' of ' + str(total) + ' - ' + name) -# count += 1 -# sleep(1) -# -# if imgs: -# total = len(imgs) -# fetch_images(total, count=1) -# else: -# print("No images found") -# -#if __name__ == "__main__": -# main() \ No newline at end of file diff --git a/porn/neko/imgs_NekoIRL/side-top.jpg b/porn/neko/imgs_NekoIRL/side-top.jpg deleted file mode 100644 index b3a3b94..0000000 Binary files a/porn/neko/imgs_NekoIRL/side-top.jpg and /dev/null differ