updated after a while because local stuff

This commit is contained in:
2017-11-10 22:58:01 +01:00
parent 4e44b73542
commit 21df9b0df5
95 changed files with 27441 additions and 25 deletions
+22 -8
View File
@@ -1,5 +1,6 @@
import asyncio import asyncio
import discord import discord
import sys, os
from discord.ext import commands from discord.ext import commands
from sympy import latex, symbols, preview, Symbol #for LaTeX images from sympy import latex, symbols, preview, Symbol #for LaTeX images
@@ -11,17 +12,30 @@ class General(object):
self.voice_states = {} self.voice_states = {}
def __check(self, ctx): #Todo: Is dit het porno kanaal? def __check(self, ctx): #Todo: Is dit het porno kanaal?
return True return True
@commands.command(pass_context=True) @commands.command()
@asyncio.coroutine @asyncio.coroutine
def tex(self, ctx, mathstring): def tex(self, ctx, *, arg):
"""Geeft een mooi LaTeX plaatje terug. """Geeft een mooi LaTeX plaatje terug.
Encapsuleer uw string in mooie \"
Bijvoorbeeld: Bijvoorbeeld:
!tex "a^b b^c \rightarrow d" !tex Je moeder is lelijk! $a^b b^c \\rightarrow d$
""" """
preamble = "\\documentclass[10pt]{article}\\pagestyle{empty}\\usepackage[margin=1in]{geometry}\\usepackage[T1]{fontenc}\\usepackage{CJKutf8}\\usepackage[english]{babel}\\usepackage{amsmath, amsfonts}\\begin{document}" preamble = "\\documentclass[10pt]{article}\\pagestyle{empty}\\usepackage[margin=1in]{geometry}\\usepackage{amsmath, amsfonts}\\begin{document}" #\\usepackage{CJKutf8}\\usepackage[T1]{fontenc}\\usepackage[english]{babel}
options = ["-T", "tight", "-z", "9", "--truecolor", "-D", "512"] options = ["-T", "tight", "-z", "9", "--truecolor", "-D", "512"]
preview(r'$$'+mathstring+'$$', viewer='file', filename='test.png', dvioptions=options, preamble=preamble) preview(r''+arg+'', viewer='file', filename='test.png', dvioptions=options, preamble=preamble)
file = open('test.png', 'rb') #file = open('test.png', 'rb')
yield from self.bot.upload(file, filename ='test.png', content='Nerd!') #yield from ctx.channel.send(file, filename ='test.png', content='')
try:
yield from ctx.message.delete()
except:
print("No permission to delete message")
yield from ctx.channel.send(ctx.author.mention, file=discord.File('test.png', 'tex.png'))
@commands.command(pass_context=True, hidden=True)
@asyncio.coroutine
def reboot(self, ctx, password=""):
""" Reboot.. """
if password == "please":
yield from ctx.channel.send("Rebooting.. brb!")
os.execv(sys.executable, ['python3'] + sys.argv)
else:
yield from ctx.channel.send(ctx.author.mention + " What's the magic word?")
+42 -5
View File
@@ -2,6 +2,7 @@ import asyncio
import discord import discord
from discord.ext import commands from discord.ext import commands
import os import os
import praw
from random import randint from random import randint
class Porno(object): class Porno(object):
@@ -11,6 +12,12 @@ class Porno(object):
self.bot = bot self.bot = bot
self.voice_states = {} self.voice_states = {}
self.fileIndex = {} self.fileIndex = {}
self.r = praw.Reddit(
user_agent='StroopwafelBot',
client_secret='Z4K17T8TC26cNAgDSRxCkE9jYR4',
client_id='aWeyKpzdHMtmJQ'
)
self.r.read_only = True
def __check(self, ctx): #Todo: Is dit het porno kanaal? def __check(self, ctx): #Todo: Is dit het porno kanaal?
return True return True
@@ -29,8 +36,38 @@ class Porno(object):
@commands.command(pass_context=True) @commands.command(pass_context=True)
@asyncio.coroutine @asyncio.coroutine
def ancilla(self, ctx): def nsfw(self, ctx):
""" :-) """ """ random post uit een nsfw subreddit, ... """
filepath = self.randomJpgFromFolder('ancilla'); print(type(ctx.message.channel))
file = open(filepath, 'rb') try:
yield from self.bot.upload(file, filename = filepath, content='Alsjeblieft!') if(not ctx.message.channel.is_nsfw()):
yield from ctx.channel.send("Alleen toegestaan in een NSFW kanaal")
return
except:
print("borf")
randnsfw = self.r.subreddit('randnsfw')
post = randnsfw.random()
try:
yield from ctx.message.delete()
except:
print("No permission to delete message")
yield from ctx.channel.send(ctx.author.mention + " Uit r/"+ randnsfw.display_name +": " + post.url)
@commands.command(pass_context=True)
@asyncio.coroutine
def gay(self, ctx):
""" Gay & NSFW. Wat wilt een mens nog meer? """
print(type(ctx.message.channel))
try:
if(not ctx.message.channel.is_nsfw()):
yield from ctx.channel.send("Alleen toegestaan in een NSFW kanaal")
return
except:
print("borf")
gaynsfw = self.r.subreddit('autofellatio+Balls+baramanga+BHMGoneWild+bigbrotherbros+Bisexy+blackballed+blackcock+blackmale+Boners+BonersInPublic+broslikeus+Bulge+Bulges+CelebrityPenis+chesthairporn+ChubbySilverDaddies+circumcisions+cock+CockOutline+CubsGoneWild+CumCannonAddicts+CuteGuyButts+daddybears+DickSlips+FMN+foreskin+gayasshole+gaybears+gaybrosgonemild+GaybrosGoneWild+GayChubs+gaycumsluts+GayDaddiesPics+GayGifs+gayholdthemoan+gayincest+GayKink+gaymersgonewild+GayMuscleWorship+gaynsfw+gayotters+gayporn+gaypornhunters+gaypornornotgayporn+gaypornwithplot+GaySnapchatImages+gaystoriesgonewild+gaystrugglefuck+gayvideos+GayWatersports+gfur+gingerdudes+grower+guyskissing+HeSquats+Homosexual+hotguyswithtattoos+insanelyhairymen+jockstraps+jockwatch+ladybonersgw+leathermen+LegionairresGoneWild+LGBTGoneWild+LGBTreesGoneWild+lovegaymale+MaleArmpits+malemodels+malepornstars+malespandex+MaleUnderwear+manass+mangonewild+manlove+ManSex+MassiveCock+MensHighJinx+menslegs+MrBlowJob+nocategory+nopullingout+NSFW_GAY+NSFW_gay_porn+OhCumOn+penis+POCLadyBoners+PublicBoys+RedditorCum+SceneBoys+selfservice+Singlets+softies+speedos+TotallyStraight+TwinkLove+twinks+UniformedMen+VintageGayPics')
post = gaynsfw.random()
try:
yield from ctx.message.delete()
except:
print("No permission to delete message")
yield from ctx.channel.send(ctx.author.mention + " " + post.url)
+74 -10
View File
@@ -16,12 +16,38 @@ class Shitpost(object):
def __check(self, ctx): #Todo: Is dit het shitpost kanaal? def __check(self, ctx): #Todo: Is dit het shitpost kanaal?
return True return True
@commands.command(pass_context=True) @commands.command(pass_context=True, hidden=True)
@asyncio.coroutine
def mark(self, ctx, boven: str, onder: str):
"""Maakt een mark meem.
Bijvoorbeeld:
!mark \"je moeder\" \"is een hoer\"
"""
params = dict(
template_id = '112898841', #de marku
username = 'Stroopwafel',
password = 'ruyter118',
text0 = boven,
text1 = onder
)
#self.bot.say("deed ik het ding?")
resp = requests.get(url='https://api.imgflip.com/caption_image', params=params)
#todo check for success
photourl = resp.json()['data']['url']
em = discord.Embed(title='De Mark heeft gesproken.', type='photo')
em.set_image(url=photourl)
try:
yield from ctx.message.delete()
except:
print("No permission to delete message")
yield from ctx.channel.send(ctx.author.mention, embed=em)
@commands.command(pass_context=True, hidden=True)
@asyncio.coroutine @asyncio.coroutine
def eriku(self, ctx, boven: str, onder: str): def eriku(self, ctx, boven: str, onder: str):
"""Maakt een erik meem. """Maakt een erik meem.
Bijvoorbeeld: Bijvoorbeeld:
@Stroopwafel eriku \"je moeder\" \"is een hoer\" !eriku \"je moeder\" \"is een hoer\"
""" """
params = dict( params = dict(
template_id = '93296473', #de eriku template_id = '93296473', #de eriku
@@ -30,18 +56,50 @@ class Shitpost(object):
text0 = boven, text0 = boven,
text1 = onder text1 = onder
) )
self.bot.say("deed ik het ding?") #self.bot.say("deed ik het ding?")
resp = requests.get(url='https://api.imgflip.com/caption_image', params=params) resp = requests.get(url='https://api.imgflip.com/caption_image', params=params)
#todo check for success #todo check for success
photourl = resp.json()['data']['url'] photourl = resp.json()['data']['url']
em = discord.Embed(title='De Eriku heeft gesproken. Prijs hem!', type='photo') em = discord.Embed(title='De Eriku heeft gesproken. Prijs hem!', type='photo')
em.set_image(url=photourl) em.set_image(url=photourl)
yield from self.bot.say(embed=em) try:
yield from ctx.message.delete()
except:
print("No permission to delete message")
yield from ctx.channel.send(ctx.author.mention, embed=em)
@commands.command(pass_context=True, hidden=True)
@asyncio.coroutine
def zout(self, ctx, boven: str, onder: str):
"""Maakt een martijn-zout meem.
Bijvoorbeeld:
!zout \"je moeder\" \"is een hoer\"
"""
params = dict(
template_id = '116231806', #de martijn
username = 'Stroopwafel',
password = 'ruyter118',
text0 = boven,
text1 = onder
)
#self.bot.say("deed ik het ding?")
resp = requests.get(url='https://api.imgflip.com/caption_image', params=params)
#todo check for success
photourl = resp.json()['data']['url']
em = discord.Embed(title='De Martijn heeft gesproken.', type='photo')
em.set_image(url=photourl)
try:
yield from ctx.message.delete()
except:
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=True)
@asyncio.coroutine @asyncio.coroutine
def nihao(self,ctx): def nihao(self,ctx):
yield from self.bot.say("Kankerlauw") #yield from self.bot.say("Kankerlauw")
yield from ctx.channel.send("kankerlauw")
@commands.command(pass_context=True) @commands.command(pass_context=True)
@asyncio.coroutine @asyncio.coroutine
@@ -49,7 +107,7 @@ class Shitpost(object):
"""Vertelt je hoe laat het is.. """Vertelt je hoe laat het is..
Misschien geeft het ook wel een leuke meem terug. Misschien geeft het ook wel een leuke meem terug.
Wie weet. Wie weet.
Probeer het eens op verschillende tijdstippen Probeer het eens op verschillende tijdstippen.
""" """
time=datetime.now().strftime('%H%M') time=datetime.now().strftime('%H%M')
@@ -64,9 +122,15 @@ class Shitpost(object):
filepath = filejpg filepath = filejpg
elif os.path.isfile(filepng): elif os.path.isfile(filepng):
filepath = filepng filepath = filepng
try:
yield from ctx.message.delete()
except:
print("No permission to delete message")
if filepath is not '': if filepath is not '':
file = open(filepath, 'rb') #file = open(filepath, 'rb')
yield from self.bot.upload(file, filename = filepath) #yield from self.bot.upload(file, filename = filepath)
yield from ctx.channel.send(ctx.author.mention + ' Het is nu ' + datetime.now().strftime('%H:%M'), file=discord.File(filepath, 'tijd.png'))
else: else:
yield from self.bot.say('Het is nu ' + datetime.now().strftime('%H:%M')) yield from ctx.channel.send(ctx.author.mention + ' Het is nu ' + datetime.now().strftime('%H:%M'))
def setup(bot):
bot.add_cog(Shitpost(bot))
+8 -2
View File
@@ -1,14 +1,20 @@
import asyncio import asyncio
import discord import discord
import Music #import Music
import Porno import Porno
import Shitpost import Shitpost
import General import General
import logging
import importlib
from discord.ext import commands from discord.ext import commands
logging.basicConfig(level=logging.INFO)
bot = commands.Bot(command_prefix=commands.when_mentioned_or('!'), description='Stroopwafel. Shitpost bot extraordinaire.', pm_help=True) bot = commands.Bot(command_prefix=commands.when_mentioned_or('!'), description='Stroopwafel. Shitpost bot extraordinaire.', pm_help=True)
bot.add_cog(Music.Music(bot)) #bot.add_cog(Music.Music(bot))
bot.add_cog(Shitpost.Shitpost(bot)) bot.add_cog(Shitpost.Shitpost(bot))
#importlib.import_module('Shitpost')
bot.add_cog(Porno.Porno(bot)) bot.add_cog(Porno.Porno(bot))
bot.add_cog(General.General(bot)) bot.add_cog(General.General(bot))
+11
View File
@@ -0,0 +1,11 @@
*.json
*.pyc
*.log
docs/_build
*.buildinfo
*.mp3
*.m4a
*.wav
*.png
*.jpg
*.flac
+21
View File
@@ -0,0 +1,21 @@
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.
+4
View File
@@ -0,0 +1,4 @@
include README.md
include LICENSE
include requirements.txt
include discord/bin/*.dll
+106
View File
@@ -0,0 +1,106 @@
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 <http://discordpy.rtfd.org/en/latest>`__.
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 <https://discord.gg/0SBTUU1wZTUzBx2q>`__ 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.
+61
View File
@@ -0,0 +1,61 @@
# -*- 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())
+284
View File
@@ -0,0 +1,284 @@
# -*- 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='<prefix>')
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: <name>)', 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()
+995
View File
@@ -0,0 +1,995 @@
# -*- 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 <discord_api_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
+340
View File
@@ -0,0 +1,340 @@
# -*- 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 '<AuditLogDiff attrs={0!r}>'.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 '<AuditLogEntry id={0.id} action={0.action} user={0.user!r}>'.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)
+85
View File
@@ -0,0 +1,85 @@
# -*- 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)
+156
View File
@@ -0,0 +1,156 @@
# -*- 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)
+930
View File
@@ -0,0 +1,930 @@
# -*- 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 '<TextChannel id={0.id} name={0.name!r} position={0.position}>'.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 '<VoiceChannel id={0.id} name={0.name!r} position={0.position}>'.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 '<CategoryChannel id={0.id} name={0.name!r} position={0.position}>'.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 '<DMChannel id={0.id} recipient={0.recipient!r}>'.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 '<GroupChannel id={0.id} name={0.name!r}>'.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
File diff suppressed because it is too large Load Diff
+222
View File
@@ -0,0 +1,222 @@
# -*- 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 '<Colour value=%s>' % 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
+140
View File
@@ -0,0 +1,140 @@
# -*- 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
@@ -0,0 +1,72 @@
# -*- 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()
+478
View File
@@ -0,0 +1,478 @@
# -*- 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
+239
View File
@@ -0,0 +1,239 @@
# -*- 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 '<Emoji id={0.id} name={0.name!r}>'.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)
+221
View File
@@ -0,0 +1,221 @@
# -*- 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
+166
View File
@@ -0,0 +1,166 @@
# -*- 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))
@@ -0,0 +1,19 @@
# -*- 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 *
@@ -0,0 +1,995 @@
# -*- 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
@@ -0,0 +1,227 @@
# -*- 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
@@ -0,0 +1,481 @@
# -*- 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<hex>``
- ``#<hex>``
- ``0x#<hex>``
- 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)
@@ -0,0 +1,130 @@
# -*- 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 '<Cooldown rate: {0.rate} per: {0.per} window: {0._window} tokens: {0._tokens}>'.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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,180 @@
# -*- 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)
@@ -0,0 +1,346 @@
# -*- 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 <subcommand chain> -> same as above
# <description>
# <command signature with aliases>
# <long doc>
# Cog:
# <command> <shortdoc>
# <command> <shortdoc>
# Other Cog:
# <command> <shortdoc>
# No Category:
# <command> <shortdoc>
# Type <prefix>help command for more info on a command.
# You can also type <prefix>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 = '<Paginator prefix: {0.prefix} suffix: {0.suffix} max_size: {0.max_size} count: {0._count}>'
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:
# <description> portion
self._paginator.add_line(description, empty=True)
if isinstance(self.command, Command):
# <signature portion>
signature = self.get_command_signature()
self._paginator.add_line(signature, empty=True)
# <long doc> 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
@@ -0,0 +1,167 @@
# -*- 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 '<StringView pos: {0.index} prev: {0.previous} end: {0.end} eof: {0.eof}>'.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)
+75
View File
@@ -0,0 +1,75 @@
# -*- 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()
+87
View File
@@ -0,0 +1,87 @@
# -*- 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 '<Game name={0.name!r} type={0.type!r} url={0.url!r}>'.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)
+699
View File
@@ -0,0 +1,699 @@
# -*- 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('<H', recv, len(recv) - 2)[0]
log.debug('detected ip: %s port: %s', state.ip, state.port)
yield from self.select_protocol(state.ip, state.port)
log.info('selected the voice protocol for use')
@asyncio.coroutine
def load_secret_key(self, data):
log.info('received secret key for voice connection')
self._connection.secret_key = data.get('secret_key')
yield from self.speak()
@asyncio.coroutine
def poll_event(self):
try:
msg = yield from asyncio.wait_for(self.recv(), timeout=30.0, loop=self.loop)
yield from self.received_message(json.loads(msg))
except websockets.exceptions.ConnectionClosed as e:
raise ConnectionClosed(e, shard_id=None) from e
@asyncio.coroutine
def close_connection(self, force=False):
if self._keep_alive:
self._keep_alive.stop()
yield from super().close_connection(force=force)
File diff suppressed because it is too large Load Diff
+759
View File
@@ -0,0 +1,759 @@
# -*- 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))
+158
View File
@@ -0,0 +1,158 @@
# -*- 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 '<Invite code={0.code!r}>'.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)
+484
View File
@@ -0,0 +1,484 @@
# -*- 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))
+559
View File
@@ -0,0 +1,559 @@
# -*- 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 '<VoiceState self_mute={0.self_mute} self_deaf={0.self_deaf} channel={0.channel!r}>'.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 '<Member id={1.id} name={1.name!r} discriminator={1.discriminator!r}' \
' bot={1.bot} nick={0.nick!r} guild={0.guild!r}>'.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)
+742
View File
@@ -0,0 +1,742 @@
# -*- 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 '<Message id={0.id} pinned={0.pinned} author={0.author!r}>'.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!",
"Wheres {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)
+42
View File
@@ -0,0 +1,42 @@
# -*- 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
+69
View File
@@ -0,0 +1,69 @@
# -*- 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)
+278
View File
@@ -0,0 +1,278 @@
# -*- 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()
+608
View File
@@ -0,0 +1,608 @@
# -*- 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 '<Permissions value=%s>' % 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)
+330
View File
@@ -0,0 +1,330 @@
# -*- 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()
+163
View File
@@ -0,0 +1,163 @@
# -*- 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 '<Reaction emoji={0.emoji!r} me={0.me} count={0.count}>'.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)
@@ -0,0 +1,82 @@
# -*- 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 '<Relationship user={0.user!r} type={0.type!r}>'.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)
+284
View File
@@ -0,0 +1,284 @@
# -*- 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 '<Role id={0.id} name={0.name!r}>'.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)
+346
View File
@@ -0,0 +1,346 @@
# -*- 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
+955
View File
@@ -0,0 +1,955 @@
# -*- 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)
+617
View File
@@ -0,0 +1,617 @@
# -*- 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 '<ClientUser id={0.id} name={0.name!r} discriminator={0.discriminator!r}' \
' bot={0.bot} verified={0.verified} mfa_enabled={0.mfa_enabled}>'.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 '<User id={0.id} name={0.name!r} discriminator={0.discriminator!r} bot={0.bot}>'.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'])
+294
View File
@@ -0,0 +1,294 @@
# -*- 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)
+426
View File
@@ -0,0 +1,426 @@
# -*- 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)
+659
View File
@@ -0,0 +1,659 @@
# -*- 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 <http://docs.python-requests.org/en/latest/api/#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 '<Webhook id=%r>' % 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<id>[0-9]{17,21})/(?P<token>[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)
+183
View File
@@ -0,0 +1,183 @@
# 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 <target>' where <target> 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."
+31
View File
@@ -0,0 +1,31 @@
$(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');
}
});
});
+542
View File
@@ -0,0 +1,542 @@
/* 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;
}
}
+29
View File
@@ -0,0 +1,29 @@
{%- extends "basic/layout.html" %}
{% set show_source = False %}
{% set style = 'style.css' %}
{%- block extrahead %}
{{ super() }}
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9">
{% endblock %}
{%- block relbar2 %}{% endblock %}
{% block header %}
{{ super() }}
{% if pagename == 'index' %}
<div class="indexwrapper">
{% endif %}
{% endblock %}
{%- block footer %}
<div class="footer">
&copy; Copyright {{ copyright }}.
{% trans sphinx_version=sphinx_version|e %}Created using <a href="http://sphinx-doc.org/">Sphinx</a> {{ sphinx_version }}.{% endtrans %}
</div>
{% if pagename == 'index' %}
</div>
{% endif %}
{%- endblock %}
+1
View File
@@ -0,0 +1 @@
<!-- purposefully empty -->
File diff suppressed because it is too large Load Diff
+314
View File
@@ -0,0 +1,314 @@
# -*- 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
# "<project> v<release> 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 <link> 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')
+97
View File
@@ -0,0 +1,97 @@
.. _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 <https://discordapp.com>`_.
2. Navigate to the `application page <https://discordapp.com/developers/applications/me>`_
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`.
@@ -0,0 +1,225 @@
.. 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 <discord-api-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:
@@ -0,0 +1,587 @@
.. 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 <https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range>`_,
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 <ext_commands_error_handler>`.
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 <ext_commands_error_handler>`. 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
@@ -0,0 +1,14 @@
``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
@@ -0,0 +1,55 @@
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('</details>\n')
def depart_summary_node(self, node):
self.body.append('</summary>')
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)
+333
View File
@@ -0,0 +1,333 @@
.. 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 <https://docs.python.org/3.5/library/asyncio-task.html#asyncio.run_coroutine_threadsafe>`_ 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. <https://github.com/Rapptz/discord.py/blob/rewrite/examples/background_task.py>`_
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 <https://discord.gg/discord-api>`_. Others look at the
source code of `other existing bots <https://github.com/Rapptz/RoboDanny>`_.
There is a `basic example <https://github.com/Rapptz/discord.py/blob/rewrite/examples/basic_bot.py>`_ 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``.
+59
View File
@@ -0,0 +1,59 @@
.. 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`
+112
View File
@@ -0,0 +1,112 @@
.. 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. <https://docs.python.org/3/tutorial/venv.html>`_
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 <discord-api-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')
+46
View File
@@ -0,0 +1,46 @@
.. 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
+263
View File
@@ -0,0 +1,263 @@
@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 ^<target^>` where ^<target^> 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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,322 @@
: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 <https://docs.python.org/3/library/asyncio-task.html>`_. 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 <https://docs.python.org/3/library/enum.html>`_ 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()
+76
View File
@@ -0,0 +1,76 @@
.. _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.
+364
View File
@@ -0,0 +1,364 @@
.. 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.
@@ -0,0 +1,28 @@
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')
+65
View File
@@ -0,0 +1,65 @@
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')
+132
View File
@@ -0,0 +1,132 @@
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')
+21
View File
@@ -0,0 +1,21 @@
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')
+20
View File
@@ -0,0 +1,20 @@
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')
@@ -0,0 +1,36 @@
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')
+15
View File
@@ -0,0 +1,15 @@
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')
+246
View File
@@ -0,0 +1,246 @@
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')
+19
View File
@@ -0,0 +1,19 @@
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')
+2
View File
@@ -0,0 +1,2 @@
aiohttp>=2.0.0,<2.3.0
websockets>=3.1,<4.0
+68
View File
@@ -0,0 +1,68 @@
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',
]
)
+43
View File
@@ -0,0 +1,43 @@
import asyncio
import discord
from discord.ext import commands
import os
import praw
from random import randint
class Porno(object):
"""Porno commando's. Jij weet wat er aan is."""
def __init__(self, bot):
self.bot = bot
self.voice_states = {}
self.fileIndex = {}
self.r = praw.Reddit(
user_agent='StroopwafelBot',
client_secret='Z4K17T8TC26cNAgDSRxCkE9jYR4',
client_id='aWeyKpzdHMtmJQ'
)
self.r.read_only = True
def __check(self, ctx): #Todo: Is dit het porno kanaal?
return True
def randomJpgFromFolder(self, dir):
if dir not in self.fileIndex.keys():
self.fileIndex[dir] = [] #empty list
for dirname, dirnames, filenames in os.walk('./'+ dir):
for filename in filenames:
if filename.endswith('.jpg'):
self.fileIndex[dir].append(os.path.join(dirname, filename))
#now we should have an index for the selected folder, but it might still be empty (i.e. empty folder given)
if self.fileIndex[dir]:
return self.fileIndex[dir][randint(0,len(self.fileIndex[dir])-1)] #get a random element from the newly created index
else:
return 'default file here'
@commands.command(pass_context=True)
@asyncio.coroutine
def nsfw(self, ctx):
""" random post uit een nsfw subreddit, for reasons.. """
randnsfw = self.r.subreddit('randnsfw')
post = randnsfw.random()
yield from self.bot.say(post.url)
+27
View File
@@ -0,0 +1,27 @@
import asyncio
import discord
from discord.ext import commands
from sympy import latex, symbols, preview, Symbol #for LaTeX images
class General(object):
"""Overal toegestaan."""
def __init__(self, bot):
self.bot = bot
self.voice_states = {}
def __check(self, ctx): #Todo: Is dit het porno kanaal?
return True
@commands.command(pass_context=True)
+240
View File
@@ -0,0 +1,240 @@
import asyncio
import discord
from discord.ext import commands
class VoiceEntry:
def __init__(self, message, player):
self.requester = message.author
self.channel = message.channel
self.player = player
def __str__(self):
fmt = '*{0.title}* verzocht door {1.display_name}'
duration = self.player.duration
if duration:
fmt = fmt + ' [{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)
@asyncio.coroutine
def audio_player_task(self):
while True:
self.play_next_song.clear()
self.current = yield from self.songs.get()
yield from self.bot.send_message(self.current.channel, 'U luistert naar: ' + str(self.current))
self.current.player.start()
yield from self.play_next_song.wait()
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 Music:
"""Play that funky music white boy.
"""
def __init__(self, bot):
self.bot = bot
self.voice_states = {}
def __check(self, ctx):
return True
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
@asyncio.coroutine
def create_voice_client(self, channel):
voice = yield from 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)
@asyncio.coroutine
def join(self, ctx, *, channel : discord.Channel):
"""Verplaats stroopie naar een bepaald kanaal."""
try:
yield from self.create_voice_client(channel)
except discord.ClientException:
yield from self.bot.say('Already in a voice channel...')
except discord.InvalidArgument:
yield from self.bot.say('This is not a voice channel...')
else:
yield from self.bot.say('Ready to play audio in ' + channel.name)
@commands.command(pass_context=True, no_pm=True)
@asyncio.coroutine
def summon(self, ctx):
"""Roep stroopie naar jouw kanaal toe.."""
summoned_channel = ctx.message.author.voice_channel
if summoned_channel is None:
yield from self.bot.say('You are not in a voice channel.')
return False
state = self.get_voice_state(ctx.message.server)
+36
View File
@@ -0,0 +1,36 @@
import asyncio
import discord
from discord.ext import commands
import os
from random import randint
class Porno(object):
"""Porno commando's. Jij weet wat er aan is."""
def __init__(self, bot):
self.bot = bot
self.voice_states = {}
self.fileIndex = {}
def __check(self, ctx): #Todo: Is dit het porno kanaal?
return True
def randomJpgFromFolder(self, dir):
if dir not in self.fileIndex.keys():
+72
View File
@@ -0,0 +1,72 @@
import asyncio
import discord
from discord.ext import commands
from urllib import request
import json, requests
from datetime import datetime
import sys
import os
class Shitpost(object):
"""Schijtpaal commando's. Alleen bruikbaar in de daarvoor toegewezen schijtpaal kanalen."""
def __init__(self, bot):
self.bot = bot
self.voice_states = {}
def __check(self, ctx): #Todo: Is dit het shitpost kanaal?
return True
@commands.command(pass_context=True)
@asyncio.coroutine
def eriku(self, ctx, boven: str, onder: str):
"""Maakt een erik meem.
Bijvoorbeeld:
@Stroopwafel eriku \"je moeder\" \"is een hoer\"
"""
params = dict(
template_id = '93296473', #de eriku
username = 'Stroopwafel',
password = 'ruyter118',
text0 = boven,
text1 = onder
)
self.bot.say("deed ik het ding?")
resp = requests.get(url='https://api.imgflip.com/caption_image', params=params)
#todo check for success
photourl = resp.json()['data']['url']
+20
View File
@@ -0,0 +1,20 @@
import asyncio
import discord
import Music
import Porno
import Shitpost
import General
from discord.ext import commands
bot = commands.Bot(command_prefix=commands.when_mentioned_or('!'), description='Stroopwafel. Shitpost bot extraordinaire.', pm_help=True)
bot.add_cog(Music.Music(bot))
+58
View File
@@ -0,0 +1,58 @@
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()
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB