updated after a while because local stuff
This commit is contained in:
+22
-8
@@ -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?")
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
*.json
|
||||||
|
*.pyc
|
||||||
|
*.log
|
||||||
|
docs/_build
|
||||||
|
*.buildinfo
|
||||||
|
*.mp3
|
||||||
|
*.m4a
|
||||||
|
*.wav
|
||||||
|
*.png
|
||||||
|
*.jpg
|
||||||
|
*.flac
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
include README.md
|
||||||
|
include LICENSE
|
||||||
|
include requirements.txt
|
||||||
|
include discord/bin/*.dll
|
||||||
@@ -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.
|
||||||
|
|
||||||
@@ -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())
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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))
|
||||||
@@ -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)
|
||||||
@@ -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))
|
||||||
@@ -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)
|
||||||
@@ -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!",
|
||||||
|
"Where’s {0}? In the server!",
|
||||||
|
"{0} hopped into the server. Kangaroo!!",
|
||||||
|
"{0} just showed up. Hold my beer.",
|
||||||
|
"Challenger approaching - {0} has appeared!",
|
||||||
|
"It's a bird! It's a plane! Nevermind, it's just {0}.",
|
||||||
|
"It's {0}! Praise the sun! [T]/",
|
||||||
|
"Never gonna give {0} up. Never gonna let {0} down.",
|
||||||
|
"Ha! {0} has joined! You activated my trap card!",
|
||||||
|
"Cheers, love! {0}'s here!",
|
||||||
|
"Hey! Listen! {0} has joined!",
|
||||||
|
"We've been expecting you {0}",
|
||||||
|
"It's dangerous to go alone, take {0}!",
|
||||||
|
"{0} has joined the server! It's super effective!",
|
||||||
|
"Cheers, love! {0} is here!",
|
||||||
|
"{0} is here, as the prophecy foretold.",
|
||||||
|
"{0} has arrived. Party's over.",
|
||||||
|
"Ready player {0}",
|
||||||
|
"{0} is here to kick butt and chew bubblegum. And {0} is all out of gum.",
|
||||||
|
"Hello. Is it {0} you're looking for?",
|
||||||
|
"{0} has joined. Stay a while and listen!",
|
||||||
|
"Roses are red, violets are blue, {0} joined this server with you",
|
||||||
|
]
|
||||||
|
|
||||||
|
index = int(self.created_at.timestamp()) % len(formats)
|
||||||
|
return formats[index].format(self.author.name)
|
||||||
|
|
||||||
|
if self.type is MessageType.call:
|
||||||
|
# we're at the call message type now, which is a bit more complicated.
|
||||||
|
# we can make the assumption that Message.channel is a PrivateChannel
|
||||||
|
# with the type ChannelType.group or ChannelType.private
|
||||||
|
call_ended = self.call.ended_timestamp is not None
|
||||||
|
|
||||||
|
if self.channel.me in self.call.participants:
|
||||||
|
return '{0.author.name} started a call.'.format(self)
|
||||||
|
elif call_ended:
|
||||||
|
return 'You missed a call from {0.author.name}'.format(self)
|
||||||
|
else:
|
||||||
|
return '{0.author.name} started a call \N{EM DASH} Join the call.'.format(self)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def delete(self):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Deletes the message.
|
||||||
|
|
||||||
|
Your own messages could be deleted without any proper permissions. However to
|
||||||
|
delete other people's messages, you need the :attr:`~Permissions.manage_messages`
|
||||||
|
permission.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
Forbidden
|
||||||
|
You do not have proper permissions to delete the message.
|
||||||
|
HTTPException
|
||||||
|
Deleting the message failed.
|
||||||
|
"""
|
||||||
|
yield from self._state.http.delete_message(self.channel.id, self.id)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def edit(self, **fields):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Edits the message.
|
||||||
|
|
||||||
|
The content must be able to be transformed into a string via ``str(content)``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
content: Optional[str]
|
||||||
|
The new content to replace the message with.
|
||||||
|
Could be ``None`` to remove the content.
|
||||||
|
embed: Optional[:class:`Embed`]
|
||||||
|
The new embed to replace the original with.
|
||||||
|
Could be ``None`` to remove the embed.
|
||||||
|
delete_after: Optional[float]
|
||||||
|
If provided, the number of seconds to wait in the background
|
||||||
|
before deleting the message we just edited. If the deletion fails,
|
||||||
|
then it is silently ignored.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
HTTPException
|
||||||
|
Editing the message failed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = fields['content']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if content is not None:
|
||||||
|
fields['content'] = str(content)
|
||||||
|
|
||||||
|
try:
|
||||||
|
embed = fields['embed']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if embed is not None:
|
||||||
|
fields['embed'] = embed.to_dict()
|
||||||
|
|
||||||
|
data = yield from self._state.http.edit_message(self.id, self.channel.id, **fields)
|
||||||
|
self._update(channel=self.channel, data=data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
delete_after = fields['delete_after']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if delete_after is not None:
|
||||||
|
@asyncio.coroutine
|
||||||
|
def delete():
|
||||||
|
yield from asyncio.sleep(delete_after, loop=self._state.loop)
|
||||||
|
try:
|
||||||
|
yield from self._state.http.delete_message(self.channel.id, self.id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
compat.create_task(delete(), loop=self._state.loop)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def pin(self):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Pins the message. You must have :attr:`~Permissions.manage_messages`
|
||||||
|
permissions to do this in a non-private channel context.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
Forbidden
|
||||||
|
You do not have permissions to pin the message.
|
||||||
|
NotFound
|
||||||
|
The message or channel was not found or deleted.
|
||||||
|
HTTPException
|
||||||
|
Pinning the message failed, probably due to the channel
|
||||||
|
having more than 50 pinned messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
yield from self._state.http.pin_message(self.channel.id, self.id)
|
||||||
|
self.pinned = True
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def unpin(self):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Unpins the message. You must have :attr:`~Permissions.manage_messages`
|
||||||
|
permissions to do this in a non-private channel context.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
Forbidden
|
||||||
|
You do not have permissions to unpin the message.
|
||||||
|
NotFound
|
||||||
|
The message or channel was not found or deleted.
|
||||||
|
HTTPException
|
||||||
|
Unpinning the message failed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
yield from self._state.http.unpin_message(self.channel.id, self.id)
|
||||||
|
self.pinned = False
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def add_reaction(self, emoji):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Add a reaction to the message.
|
||||||
|
|
||||||
|
The emoji may be a unicode emoji or a custom guild :class:`Emoji`.
|
||||||
|
|
||||||
|
You must have the :attr:`~Permissions.add_reactions` and
|
||||||
|
:attr:`~Permissions.read_message_history` permissions to use this.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialReactionEmoji`, str]
|
||||||
|
The emoji to react with.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
--------
|
||||||
|
HTTPException
|
||||||
|
Adding the reaction failed.
|
||||||
|
Forbidden
|
||||||
|
You do not have the proper permissions to react to the message.
|
||||||
|
NotFound
|
||||||
|
The emoji you specified was not found.
|
||||||
|
InvalidArgument
|
||||||
|
The emoji parameter is invalid.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(emoji, Reaction):
|
||||||
|
emoji = emoji.emoji
|
||||||
|
|
||||||
|
if isinstance(emoji, Emoji):
|
||||||
|
emoji = '%s:%s' % (emoji.name, emoji.id)
|
||||||
|
elif isinstance(emoji, PartialReactionEmoji):
|
||||||
|
emoji = emoji._as_reaction()
|
||||||
|
elif isinstance(emoji, str):
|
||||||
|
pass # this is okay
|
||||||
|
else:
|
||||||
|
raise InvalidArgument('emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.'.format(emoji))
|
||||||
|
|
||||||
|
yield from self._state.http.add_reaction(self.id, self.channel.id, emoji)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def remove_reaction(self, emoji, member):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Remove a reaction by the member from the message.
|
||||||
|
|
||||||
|
The emoji may be a unicode emoji or a custom guild :class:`Emoji`.
|
||||||
|
|
||||||
|
If the reaction is not your own (i.e. ``member`` parameter is not you) then
|
||||||
|
the :attr:`~Permissions.manage_messages` permission is needed.
|
||||||
|
|
||||||
|
The ``member`` parameter must represent a member and meet
|
||||||
|
the :class:`abc.Snowflake` abc.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialReactionEmoji`, str]
|
||||||
|
The emoji to remove.
|
||||||
|
member: :class:`abc.Snowflake`
|
||||||
|
The member for which to remove the reaction.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
--------
|
||||||
|
HTTPException
|
||||||
|
Removing the reaction failed.
|
||||||
|
Forbidden
|
||||||
|
You do not have the proper permissions to remove the reaction.
|
||||||
|
NotFound
|
||||||
|
The member or emoji you specified was not found.
|
||||||
|
InvalidArgument
|
||||||
|
The emoji parameter is invalid.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(emoji, Reaction):
|
||||||
|
emoji = emoji.emoji
|
||||||
|
|
||||||
|
if isinstance(emoji, Emoji):
|
||||||
|
emoji = '%s:%s' % (emoji.name, emoji.id)
|
||||||
|
elif isinstance(emoji, PartialReactionEmoji):
|
||||||
|
emoji = emoji._as_reaction()
|
||||||
|
elif isinstance(emoji, str):
|
||||||
|
pass # this is okay
|
||||||
|
else:
|
||||||
|
raise InvalidArgument('emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.'.format(emoji))
|
||||||
|
|
||||||
|
yield from self._state.http.remove_reaction(self.id, self.channel.id, emoji, member.id)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def clear_reactions(self):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Removes all the reactions from the message.
|
||||||
|
|
||||||
|
You need :attr:`~Permissions.manage_messages` permission
|
||||||
|
to use this.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
--------
|
||||||
|
HTTPException
|
||||||
|
Removing the reactions failed.
|
||||||
|
Forbidden
|
||||||
|
You do not have the proper permissions to remove all the reactions.
|
||||||
|
"""
|
||||||
|
yield from self._state.http.clear_reactions(self.id, self.channel.id)
|
||||||
|
|
||||||
|
def ack(self):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Marks this message as read.
|
||||||
|
|
||||||
|
The user must not be a bot user.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
HTTPException
|
||||||
|
Acking failed.
|
||||||
|
ClientException
|
||||||
|
You must not be a bot user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
state = self._state
|
||||||
|
if state.is_bot:
|
||||||
|
raise ClientException('Must not be a bot account to ack messages.')
|
||||||
|
return state.http.ack_message(self.channel.id, self.id)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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'])
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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">
|
||||||
|
© 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 %}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!-- purposefully empty -->
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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')
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -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``.
|
||||||
|
|
||||||
@@ -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`
|
||||||
|
|
||||||
@@ -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')
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -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')
|
||||||
@@ -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')
|
||||||
@@ -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')
|
||||||
@@ -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')
|
||||||
@@ -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')
|
||||||
@@ -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')
|
||||||
@@ -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')
|
||||||
@@ -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')
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
aiohttp>=2.0.0,<2.3.0
|
||||||
|
websockets>=3.1,<4.0
|
||||||
@@ -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',
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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
@@ -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():
|
||||||
|
|
||||||
@@ -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']
|
||||||
|
|
||||||
@@ -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))
|
||||||
|
|
||||||
@@ -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 |
Reference in New Issue
Block a user