mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-06 17:32:31 -05:00
Compare commits
319 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11d87067aa | ||
|
|
6c9c57c14d | ||
|
|
404c5f6dc0 | ||
|
|
f0f274e1e1 | ||
|
|
e9f014df07 | ||
|
|
778eadd418 | ||
|
|
3de9d15410 | ||
|
|
3b0567d261 | ||
|
|
49a75b5f19 | ||
|
|
8676dd3ce3 | ||
|
|
d5c412e3df | ||
|
|
1d2980f8fa | ||
|
|
3e80edcdfd | ||
|
|
be184b57dd | ||
|
|
f83f378528 | ||
|
|
21a6384ebf | ||
|
|
03e0683dd7 | ||
|
|
ac2813012a | ||
|
|
e34eca557b | ||
|
|
687b88ca6f | ||
|
|
776c75ba86 | ||
|
|
55ff9bedb7 | ||
|
|
2bdc3ac10c | ||
|
|
f2039300c2 | ||
|
|
03e59ea9d3 | ||
|
|
a89a156f8e | ||
|
|
0eb22c84ff | ||
|
|
d1593b8069 | ||
|
|
93391d028c | ||
|
|
142fb0ad08 | ||
|
|
942dca43d3 | ||
|
|
081bf663a4 | ||
|
|
55e309125e | ||
|
|
7323e8eb67 | ||
|
|
098540b9e5 | ||
|
|
10412c4f51 | ||
|
|
2f8b1a21c7 | ||
|
|
03fe3ee720 | ||
|
|
8a72840de0 | ||
|
|
8bf86f33a3 | ||
|
|
8637f8a852 | ||
|
|
bff7e214ab | ||
|
|
f3bbfdc64d | ||
|
|
461f56bca1 | ||
|
|
d1d4ec3e38 | ||
|
|
a0f34bbbd9 | ||
|
|
49819a2eeb | ||
|
|
f91d8610ae | ||
|
|
52f5d5cd6a | ||
|
|
870b615364 | ||
|
|
25ccc11dc4 | ||
|
|
f2b7ce9546 | ||
|
|
6bdc9606f6 | ||
|
|
71d0bd0d07 | ||
|
|
6ae3040aac | ||
|
|
065396abab | ||
|
|
1804314f45 | ||
|
|
ff894ecbe7 | ||
|
|
0bf54fae52 | ||
|
|
3c66c602f6 | ||
|
|
e854716236 | ||
|
|
beb16b81a9 | ||
|
|
57d5c0870a | ||
|
|
9d008d587a | ||
|
|
804d6eecea | ||
|
|
cc927248f0 | ||
|
|
d133598d80 | ||
|
|
682ee1a459 | ||
|
|
61b5730c48 | ||
|
|
463d8d6306 | ||
|
|
da40511306 | ||
|
|
c2195ec576 | ||
|
|
16443c8cc0 | ||
|
|
9116cd02e6 | ||
|
|
652d9fe950 | ||
|
|
e956e6e320 | ||
|
|
2e58922d01 | ||
|
|
33b7652b62 | ||
|
|
0e9086ca1f | ||
|
|
3ca2a9af28 | ||
|
|
e7b615d921 | ||
|
|
2cb6e98092 | ||
|
|
1ccc441aab | ||
|
|
8ddc5aa63e | ||
|
|
f894b62bfe | ||
|
|
aac9369f3f | ||
|
|
1581604f71 | ||
|
|
56161c0a88 | ||
|
|
242df83785 | ||
|
|
2338ad8223 | ||
|
|
b4f4e080af | ||
|
|
132545e057 | ||
|
|
68590dfdb8 | ||
|
|
2e271d695b | ||
|
|
cd745d35c2 | ||
|
|
6928e2aca2 | ||
|
|
49e86614c5 | ||
|
|
51dcf65fd7 | ||
|
|
c6c0165214 | ||
|
|
342935de49 | ||
|
|
ced5bb4631 | ||
|
|
0a832cee9c | ||
|
|
1cfce8b72c | ||
|
|
cdcde26dfc | ||
|
|
1ffb79f852 | ||
|
|
644aaf0c0e | ||
|
|
cdea03792d | ||
|
|
b190e7417e | ||
|
|
7dd3ff7c8d | ||
|
|
21a253103e | ||
|
|
db3fb29b30 | ||
|
|
c5d2ae5831 | ||
|
|
9d0ca00f89 | ||
|
|
79e5d2c9d7 | ||
|
|
3a62d392b4 | ||
|
|
f2858ea48c | ||
|
|
3c78fb420b | ||
|
|
2d22ee7ccc | ||
|
|
9a243a1454 | ||
|
|
165e40c0db | ||
|
|
7f1c2b475b | ||
|
|
a5f38fa6e6 | ||
|
|
598968bf74 | ||
|
|
ee661b0a9f | ||
|
|
7d7b3413bc | ||
|
|
7400008384 | ||
|
|
4e564e8ce4 | ||
|
|
8691fdc533 | ||
|
|
7d103f1d32 | ||
|
|
3de1a265ea | ||
|
|
3b6d4d9df6 | ||
|
|
e96e5374b4 | ||
|
|
5c91709ac8 | ||
|
|
96a91b9d0e | ||
|
|
65b88c09fb | ||
|
|
80ff07f53d | ||
|
|
4f6485d1f9 | ||
|
|
d1c903f36f | ||
|
|
516ebcfa2b | ||
|
|
9414de24d4 | ||
|
|
16990071cb | ||
|
|
52433d253f | ||
|
|
bb6327d969 | ||
|
|
476f441c9b | ||
|
|
c79b5e6179 | ||
|
|
c1bee3fee5 | ||
|
|
d786103d8d | ||
|
|
b3850f6bb7 | ||
|
|
e3db3c0341 | ||
|
|
1ce3bc2870 | ||
|
|
24ac111782 | ||
|
|
07f127ffe4 | ||
|
|
c58f566047 | ||
|
|
f28d6dff32 | ||
|
|
7e49ce9a7b | ||
|
|
59115cd1c7 | ||
|
|
1d93fe4cf9 | ||
|
|
3f1f7640cb | ||
|
|
83ee7c5e92 | ||
|
|
b95ddf18ba | ||
|
|
61d255726c | ||
|
|
05e2851c67 | ||
|
|
f20a174038 | ||
|
|
ec108e7c02 | ||
|
|
ad114295e7 | ||
|
|
0ff7259bc3 | ||
|
|
49af94334e | ||
|
|
22c318fda3 | ||
|
|
da5fd7699e | ||
|
|
3d498a74ba | ||
|
|
460b4bb3f2 | ||
|
|
47723cee33 | ||
|
|
a1b03be27e | ||
|
|
012d99c05c | ||
|
|
2c8a425f87 | ||
|
|
8555f8c28c | ||
|
|
46413c2c52 | ||
|
|
eaeaf9dd69 | ||
|
|
ee11d7da63 | ||
|
|
0ac93aacd5 | ||
|
|
691d8af26d | ||
|
|
87c66b2423 | ||
|
|
005123a371 | ||
|
|
bb8ce43cc0 | ||
|
|
13611e34d2 | ||
|
|
b8190c44a8 | ||
|
|
95d5ec5f0e | ||
|
|
874204bf18 | ||
|
|
6c296a9a17 | ||
|
|
ad06b0e723 | ||
|
|
0652dd344b | ||
|
|
8b3c3e89e9 | ||
|
|
c82ac5ae68 | ||
|
|
2776db0cf9 | ||
|
|
ba19179e4f | ||
|
|
e347ffa336 | ||
|
|
c85af62401 | ||
|
|
39b64b7570 | ||
|
|
56b220b92e | ||
|
|
972fbecc94 | ||
|
|
136e781c7f | ||
|
|
0852d1be9f | ||
|
|
fb722c79be | ||
|
|
c63d069f69 | ||
|
|
1cd7e41f33 | ||
|
|
d6d6d14977 | ||
|
|
82cda4b57a | ||
|
|
301c800319 | ||
|
|
30af83aa6a | ||
|
|
0f9501f93a | ||
|
|
466b2b82d0 | ||
|
|
8ab39512d9 | ||
|
|
e08e95c04e | ||
|
|
e7b1fa5ab5 | ||
|
|
14a2f98418 | ||
|
|
de7d08ee75 | ||
|
|
2a486cad66 | ||
|
|
80fc639480 | ||
|
|
c7608aeb17 | ||
|
|
050300040c | ||
|
|
94c3a2fedd | ||
|
|
421043d923 | ||
|
|
1c22b212c2 | ||
|
|
d52b3eaf21 | ||
|
|
30ca226e39 | ||
|
|
15037013e7 | ||
|
|
30fa9303e8 | ||
|
|
b4753a02de | ||
|
|
628073cbe1 | ||
|
|
2755592175 | ||
|
|
6051ccb23c | ||
|
|
46f9cae0ef | ||
|
|
16614168a7 | ||
|
|
5a15939f08 | ||
|
|
bb5aab16c9 | ||
|
|
b38ac1d025 | ||
|
|
22cf8e940c | ||
|
|
b0ab6bd7e2 | ||
|
|
7b9d85c1b5 | ||
|
|
3637804929 | ||
|
|
c70a44e0fe | ||
|
|
cf18b601e2 | ||
|
|
139cc07bda | ||
|
|
619c3f28f7 | ||
|
|
9966668307 | ||
|
|
77a0a67029 | ||
|
|
b65466cebd | ||
|
|
f1873e32d6 | ||
|
|
b7b4e65d2d | ||
|
|
16bb334fba | ||
|
|
3f1d416526 | ||
|
|
722aaa225b | ||
|
|
7e2e37ab3f | ||
|
|
83411d0fa4 | ||
|
|
d608dd953b | ||
|
|
e5e0a024f9 | ||
|
|
8e6db0829c | ||
|
|
5359fec195 | ||
|
|
f2daf0be9a | ||
|
|
b633a33137 | ||
|
|
d13bf37845 | ||
|
|
4b831a634a | ||
|
|
f91e0a6546 | ||
|
|
2e2d669fdf | ||
|
|
7ecdf7a7be | ||
|
|
9b940de854 | ||
|
|
82807ffe69 | ||
|
|
b1066ad58f | ||
|
|
ac8b1fc108 | ||
|
|
820be2a0ae | ||
|
|
9869f95bd6 | ||
|
|
c87286d3c6 | ||
|
|
7028ca9df3 | ||
|
|
435fc141ae | ||
|
|
889fa63aff | ||
|
|
dae75521d3 | ||
|
|
b9d440f2f7 | ||
|
|
ec4c325efd | ||
|
|
b350ac38dc | ||
|
|
e88c82e7e0 | ||
|
|
99ad01ae0d | ||
|
|
8f8c52d8c4 | ||
|
|
c56fa5a320 | ||
|
|
7d5bae5a50 | ||
|
|
7c404082f8 | ||
|
|
dc8e61cbe5 | ||
|
|
f2ebf52f6e | ||
|
|
fa223e22ed | ||
|
|
6d22c8faa5 | ||
|
|
01ebf2835b | ||
|
|
3ef693a259 | ||
|
|
3a4d932d2b | ||
|
|
571332ae18 | ||
|
|
0607f5552a | ||
|
|
016a6d3aa6 | ||
|
|
c7d98f88e8 | ||
|
|
b82756087a | ||
|
|
3b62572c89 | ||
|
|
abcf179042 | ||
|
|
3dba09d19d | ||
|
|
e07408161a | ||
|
|
937d2fe0f6 | ||
|
|
bcc50557a9 | ||
|
|
bdcb69ad37 | ||
|
|
3b50ed8192 | ||
|
|
30c3a4c7c1 | ||
|
|
6470bc1cda | ||
|
|
221b636f3f | ||
|
|
f7e41063bf | ||
|
|
d6cd959a2b | ||
|
|
57f078925e | ||
|
|
8b4e12da81 | ||
|
|
5ed8be9998 | ||
|
|
4357fe1ba9 | ||
|
|
a64db76b4d | ||
|
|
c464f5e7dc | ||
|
|
00bc3c86b1 | ||
|
|
ba605495ac | ||
|
|
748847d5bf |
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
@@ -9,16 +9,15 @@ redbot/core/config.py @tekulvw
|
|||||||
redbot/core/cog_manager.py @tekulvw
|
redbot/core/cog_manager.py @tekulvw
|
||||||
redbot/core/core_commands.py @tekulvw
|
redbot/core/core_commands.py @tekulvw
|
||||||
redbot/core/context.py @Tobotimus
|
redbot/core/context.py @Tobotimus
|
||||||
|
redbot/core/commands/* @mikeshardmind
|
||||||
redbot/core/data_manager.py @tekulvw
|
redbot/core/data_manager.py @tekulvw
|
||||||
redbot/core/dev_commands.py @tekulvw
|
redbot/core/dev_commands.py @tekulvw
|
||||||
redbot/core/drivers/* @tekulvw
|
redbot/core/drivers/* @tekulvw
|
||||||
redbot/core/events.py @tekulvw
|
redbot/core/events.py @tekulvw
|
||||||
redbot/core/global_checks.py @tekulvw
|
redbot/core/global_checks.py @tekulvw
|
||||||
redbot/core/i18n.py @tekulvw
|
redbot/core/i18n.py @tekulvw
|
||||||
redbot/core/json_io.py @tekulvw
|
|
||||||
redbot/core/modlog.py @palmtree5
|
redbot/core/modlog.py @palmtree5
|
||||||
redbot/core/rpc.py @tekulvw
|
redbot/core/rpc.py @tekulvw
|
||||||
redbot/core/sentry_setup.py @Kowlin @tekulvw
|
|
||||||
redbot/core/utils/chat_formatting.py @tekulvw
|
redbot/core/utils/chat_formatting.py @tekulvw
|
||||||
redbot/core/utils/mod.py @palmtree5
|
redbot/core/utils/mod.py @palmtree5
|
||||||
redbot/core/utils/data_converter.py @mikeshardmind
|
redbot/core/utils/data_converter.py @mikeshardmind
|
||||||
@@ -30,7 +29,7 @@ redbot/core/utils/common_filters.py @mikeshardmind
|
|||||||
# Cogs
|
# Cogs
|
||||||
redbot/cogs/admin/* @tekulvw
|
redbot/cogs/admin/* @tekulvw
|
||||||
redbot/cogs/alias/* @tekulvw
|
redbot/cogs/alias/* @tekulvw
|
||||||
redbot/cogs/audio/* @aikaterna @atiwiex
|
redbot/cogs/audio/* @aikaterna
|
||||||
redbot/cogs/bank/* @tekulvw
|
redbot/cogs/bank/* @tekulvw
|
||||||
redbot/cogs/cleanup/* @palmtree5
|
redbot/cogs/cleanup/* @palmtree5
|
||||||
redbot/cogs/customcom/* @palmtree5
|
redbot/cogs/customcom/* @palmtree5
|
||||||
@@ -43,7 +42,6 @@ redbot/cogs/mod/* @palmtree5
|
|||||||
redbot/cogs/modlog/* @palmtree5
|
redbot/cogs/modlog/* @palmtree5
|
||||||
redbot/cogs/streams/* @Twentysix26 @palmtree5
|
redbot/cogs/streams/* @Twentysix26 @palmtree5
|
||||||
redbot/cogs/trivia/* @Tobotimus
|
redbot/cogs/trivia/* @Tobotimus
|
||||||
redbot/cogs/dataconverter/* @mikeshardmind
|
|
||||||
redbot/cogs/reports/* @mikeshardmind
|
redbot/cogs/reports/* @mikeshardmind
|
||||||
redbot/cogs/permissions/* @mikeshardmind
|
redbot/cogs/permissions/* @mikeshardmind
|
||||||
redbot/cogs/warnings/* @palmtree5
|
redbot/cogs/warnings/* @palmtree5
|
||||||
|
|||||||
39
.github/CONTRIBUTING.md
vendored
39
.github/CONTRIBUTING.md
vendored
@@ -31,7 +31,7 @@ We love receiving contributions from our community. Any assistance you can provi
|
|||||||
# 2. Ground Rules
|
# 2. Ground Rules
|
||||||
We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub) as our main source of collaboration and coordination. Your experience contributing to Red will be greatly improved if you go get that plugin.
|
We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub) as our main source of collaboration and coordination. Your experience contributing to Red will be greatly improved if you go get that plugin.
|
||||||
1. Ensure cross compatibility for Windows, Mac OS and Linux.
|
1. Ensure cross compatibility for Windows, Mac OS and Linux.
|
||||||
2. Ensure all Python features used in contributions exist and work in Python 3.6 and above.
|
2. Ensure all Python features used in contributions exist and work in Python 3.7 and above.
|
||||||
3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning:
|
3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning:
|
||||||
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
|
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
|
||||||
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
|
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
|
||||||
@@ -53,33 +53,36 @@ Red's repository is configured to follow a particular development workflow, usin
|
|||||||
|
|
||||||
### 4.1 Setting up your development environment
|
### 4.1 Setting up your development environment
|
||||||
The following requirements must be installed prior to setting up:
|
The following requirements must be installed prior to setting up:
|
||||||
- Python 3.6.2 or greater (3.6.6 or greater on Windows)
|
- Python 3.7.0 or greater
|
||||||
- git
|
- git
|
||||||
- pip
|
- pip
|
||||||
- pipenv
|
|
||||||
|
|
||||||
If you're not on Windows, you can optionally install [pyenv](https://github.com/pyenv/pyenv), which will help you run tests for different python versions.
|
If you're not on Windows, you should also have GNU make installed, and you can optionally install [pyenv](https://github.com/pyenv/pyenv), which can help you run tests for different python versions.
|
||||||
|
|
||||||
1. Fork and clone the repository to a directory on your local machine.
|
1. Fork and clone the repository to a directory on your local machine.
|
||||||
2. Open a command line in that directory and execute the following commands:
|
2. Open a command line in that directory and execute the following command:
|
||||||
```bash
|
```bash
|
||||||
pip install pipenv
|
make newenv
|
||||||
pipenv install --dev
|
|
||||||
```
|
```
|
||||||
Red, its dependencies, and all required development tools, are now installed to a virtual environment. Red is installed in editable mode, meaning that edits you make to the source code in the repository will be reflected when you run Red.
|
Red, its dependencies, and all required development tools, are now installed to a virtual environment located in the `.venv` subdirectory. Red is installed in editable mode, meaning that edits you make to the source code in the repository will be reflected when you run Red.
|
||||||
3. Activate the new virtual environment with the command:
|
3. Activate the new virtual environment with one of the following commands:
|
||||||
```bash
|
- Posix:
|
||||||
pipenv shell
|
```bash
|
||||||
```
|
source .venv/bin/activate
|
||||||
From here onwards, we will assume you are executing commands from within this shell. Each time you open a new command line, you should execute this command first.
|
```
|
||||||
|
- Windows:
|
||||||
|
```powershell
|
||||||
|
.venv\Scripts\activate
|
||||||
|
```
|
||||||
|
Each time you open a new command line, you should execute this command first. From here onwards, we will assume you are executing commands from within this activated virtual environment.
|
||||||
|
|
||||||
Note: If you haven't used `pipenv` before but are comfortable with virtualenvs, just run `pip install pipenv` in the virtualenv you're already using and invoke the command above from the cloned Red repo. It will do the correct thing.
|
**Note:** If you're comfortable with setting up virtual environments yourself and would rather do it manually, just run `pip install -Ur tools/dev-requirements.txt` after setting it up.
|
||||||
|
|
||||||
### 4.2 Testing
|
### 4.2 Testing
|
||||||
We've recently started using [tox](https://github.com/tox-dev/tox) to run all of our tests. It's extremely simple to use, and if you followed the previous section correctly, it is already installed to your virtual environment.
|
We've recently started using [tox](https://github.com/tox-dev/tox) to run all of our tests. It's extremely simple to use, and if you followed the previous section correctly, it is already installed to your virtual environment.
|
||||||
|
|
||||||
Currently, tox does the following, creating its own virtual environments for each stage:
|
Currently, tox does the following, creating its own virtual environments for each stage:
|
||||||
- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on python 3.6 and 3.7 (test environments `py36` and `py37`)
|
- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on python 3.7 (test environment `py37`)
|
||||||
- Ensures documentation builds without warnings, and all hyperlinks have a valid destination (test environment `docs`)
|
- Ensures documentation builds without warnings, and all hyperlinks have a valid destination (test environment `docs`)
|
||||||
- Ensures that the code meets our style guide with [black](https://github.com/ambv/black) (test environment `style`)
|
- Ensures that the code meets our style guide with [black](https://github.com/ambv/black) (test environment `style`)
|
||||||
|
|
||||||
@@ -95,12 +98,14 @@ Our style checker of choice, [black](https://github.com/ambv/black), actually ha
|
|||||||
Use the command `black --help` to see how to use this tool. The full style guide is explained in detail on [black's GitHub repository](https://github.com/ambv/black). **There is one exception to this**, however, which is that we set the line length to 99, instead of black's default 88. When using `black` on the command line, simply use it like so: `black -l 99 -N <src>`.
|
Use the command `black --help` to see how to use this tool. The full style guide is explained in detail on [black's GitHub repository](https://github.com/ambv/black). **There is one exception to this**, however, which is that we set the line length to 99, instead of black's default 88. When using `black` on the command line, simply use it like so: `black -l 99 -N <src>`.
|
||||||
|
|
||||||
### 4.4 Make
|
### 4.4 Make
|
||||||
You may have noticed we have a `Makefile` and a `make.bat` in the top-level directory. For now, you can do two things with them:
|
You may have noticed we have a `Makefile` and a `make.bat` in the top-level directory. For now, you can do three things with them:
|
||||||
1. `make reformat`: Reformat all python files in the project with Black
|
1. `make reformat`: Reformat all python files in the project with Black
|
||||||
2. `make stylecheck`: Check if any `.py` files in the project need reformatting
|
2. `make stylecheck`: Check if any `.py` files in the project need reformatting
|
||||||
|
3. `make newenv`: Set up a new virtual environment in the `.venv` subdirectory, and install Red and its dependencies. If one already exists, it is cleared out and replaced.
|
||||||
|
4. `make syncenv`: Sync your environment with Red's latest dependencies.
|
||||||
|
|
||||||
### 4.5 Keeping your dependencies up to date
|
### 4.5 Keeping your dependencies up to date
|
||||||
Whenever you pull from upstream (V3/develop on the main repository) and you notice the file `Pipfile.lock` has been changed, it usually means one of the package dependencies have been updated, added or removed. To make sure you're testing and formatting with the most up-to-date versions of our dependencies, run `pipenv install --dev` again.
|
Whenever you pull from upstream (V3/develop on the main repository) and you notice either of the files `setup.cfg` or `tools/dev-requirements.txt` have been changed, it can often mean some package dependencies have been updated, added or removed. To make sure you're testing and formatting with the most up-to-date versions of our dependencies, run `make syncenv`. You could also simply do `make newenv` to install them to a clean new virtual environment.
|
||||||
|
|
||||||
### 4.6 To contribute changes
|
### 4.6 To contribute changes
|
||||||
|
|
||||||
|
|||||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
patreon: Red_Devs
|
||||||
5
.github/ISSUE_TEMPLATE.md
vendored
Normal file
5
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<!--
|
||||||
|
Please be sure to use the correct template,
|
||||||
|
if your report doesn't have the correct template please open an issue describing your issue in detail
|
||||||
|
For support regarding the bot itself please visit the discord server over at https://discord.gg/red
|
||||||
|
-->
|
||||||
6
.github/ISSUE_TEMPLATE/command_bug.md
vendored
6
.github/ISSUE_TEMPLATE/command_bug.md
vendored
@@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
name: Bug reports for commands
|
||||||
|
about: For bugs that involve commands found within Red
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Command bugs
|
# Command bugs
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/feature_req.md
vendored
6
.github/ISSUE_TEMPLATE/feature_req.md
vendored
@@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: For feature requests regarding Red itself.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Feature request
|
# Feature request
|
||||||
|
|
||||||
<!-- This template is for feature requests. Please fill out the following: -->
|
<!-- This template is for feature requests. Please fill out the following: -->
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/other_bug.md
vendored
8
.github/ISSUE_TEMPLATE/other_bug.md
vendored
@@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: For bugs that don't involve a command.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Other bugs
|
# Other bugs
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -18,4 +24,4 @@ Did you find a bug with something other than a command? Fill out the following:
|
|||||||
|
|
||||||
#### How can we reproduce this issue?
|
#### How can we reproduce this issue?
|
||||||
|
|
||||||
<!-- Replace with numbered steps to reproduce the issue -->
|
<!-- Replace with numbered steps to reproduce the issue -->
|
||||||
|
|||||||
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
### Type
|
||||||
|
|
||||||
|
- [ ] Bugfix
|
||||||
|
- [ ] Enhancement
|
||||||
|
- [ ] New feature
|
||||||
|
|
||||||
|
### Description of the changes
|
||||||
1
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
1
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
@@ -1,6 +1,7 @@
|
|||||||
# Bugfix request
|
# Bugfix request
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
THIS TEMPLATE IS CURRENTLY UNUSED DUE TO GITHUB LIMITATIONS!
|
||||||
To be used for pull requests that fix a bug
|
To be used for pull requests that fix a bug
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
3
.github/PULL_REQUEST_TEMPLATE/enhancement.md
vendored
3
.github/PULL_REQUEST_TEMPLATE/enhancement.md
vendored
@@ -1,6 +1,7 @@
|
|||||||
# Enhancement request
|
# Enhancement request
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
THIS TEMPLATE IS CURRENTLY UNUSED DUE TO GITHUB LIMITATIONS!
|
||||||
To be used for PRs which enhance existing features
|
To be used for PRs which enhance existing features
|
||||||
-->
|
-->
|
||||||
|
|
||||||
@@ -17,4 +18,4 @@ If adding commands, describe any restrictions on their usage.
|
|||||||
<!-- To check a box, replace the space between the [] with a x -->
|
<!-- To check a box, replace the space between the [] with a x -->
|
||||||
|
|
||||||
- [ ] Yes
|
- [ ] Yes
|
||||||
- [ ] No
|
- [ ] No
|
||||||
|
|||||||
3
.github/PULL_REQUEST_TEMPLATE/new_feature.md
vendored
3
.github/PULL_REQUEST_TEMPLATE/new_feature.md
vendored
@@ -1,6 +1,7 @@
|
|||||||
# New feature addition
|
# New feature addition
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
THIS TEMPLATE IS CURRENTLY UNUSED DUE TO GITHUB LIMITATIONS!
|
||||||
To be used for PRs which add a new feature
|
To be used for PRs which add a new feature
|
||||||
Examples of this include new APIs, new core cogs, etc.
|
Examples of this include new APIs, new core cogs, etc.
|
||||||
-->
|
-->
|
||||||
@@ -18,4 +19,4 @@ Examples of this include new APIs, new core cogs, etc.
|
|||||||
<!--
|
<!--
|
||||||
If you are adding a cog, describe its commands in detail (functionality, usage restrictions, etc).
|
If you are adding a cog, describe its commands in detail (functionality, usage restrictions, etc).
|
||||||
If the new feature introduces new requirements, please try to explain why they are necessary.
|
If the new feature introduces new requirements, please try to explain why they are necessary.
|
||||||
-->
|
-->
|
||||||
|
|||||||
2
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
2
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
@@ -1,6 +1,7 @@
|
|||||||
# New release
|
# New release
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
THIS TEMPLATE IS CURRENTLY UNUSED DUE TO GITHUB LIMITATIONS!
|
||||||
To be used by collaborators for doing releases.
|
To be used by collaborators for doing releases.
|
||||||
Most contributors will not need to use this.
|
Most contributors will not need to use this.
|
||||||
-->
|
-->
|
||||||
@@ -13,4 +14,3 @@ Most contributors will not need to use this.
|
|||||||
|
|
||||||
- [ ] Yes
|
- [ ] Yes
|
||||||
- [ ] No
|
- [ ] No
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Translations update
|
# Translations update
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
THIS TEMPLATE IS CURRENTLY UNUSED DUE TO GITHUB LIMITATIONS!
|
||||||
Used for PRs updating translations from Crowdin
|
Used for PRs updating translations from Crowdin
|
||||||
-->
|
-->
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,6 +4,8 @@
|
|||||||
*.pot
|
*.pot
|
||||||
.data
|
.data
|
||||||
!/tests/cogs/dataconverter/data/**/*.json
|
!/tests/cogs/dataconverter/data/**/*.json
|
||||||
|
Pipfile
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
### JetBrains template
|
### JetBrains template
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||||
|
|||||||
148
.pylintrc
Normal file
148
.pylintrc
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
[MASTER]
|
||||||
|
|
||||||
|
# Specify a configuration file.
|
||||||
|
#rcfile=
|
||||||
|
|
||||||
|
# Add files or directories to the blacklist. They should be base names, not
|
||||||
|
# paths.
|
||||||
|
ignore=pytest
|
||||||
|
|
||||||
|
# Pickle collected data for later comparisons.
|
||||||
|
persistent=no
|
||||||
|
|
||||||
|
# List of plugins (as comma separated values of python modules names) to load,
|
||||||
|
# usually to register additional checkers.
|
||||||
|
load-plugins=
|
||||||
|
|
||||||
|
# DO NOT CHANGE THIS VALUE # Use multiple processes to speed up Pylint.
|
||||||
|
jobs=1
|
||||||
|
|
||||||
|
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||||
|
# active Python interpreter and may run arbitrary code.
|
||||||
|
unsafe-load-any-extension=no
|
||||||
|
|
||||||
|
# A comma-separated list of package or module names from where C extensions may
|
||||||
|
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||||
|
# run arbitrary code
|
||||||
|
extension-pkg-whitelist=
|
||||||
|
|
||||||
|
# Allow optimization of some AST trees. This will activate a peephole AST
|
||||||
|
# optimizer, which will apply various small optimizations. For instance, it can
|
||||||
|
# be used to obtain the result of joining multiple strings with the addition
|
||||||
|
# operator. Joining a lot of strings can lead to a maximum recursion error in
|
||||||
|
# Pylint and this flag can prevent that. It has one side effect, the resulting
|
||||||
|
# AST will be different than the one from reality.
|
||||||
|
optimize-ast=no
|
||||||
|
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
|
||||||
|
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||||
|
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
|
||||||
|
confidence=
|
||||||
|
|
||||||
|
# Enable the message, report, category or checker with the given id(s). You can
|
||||||
|
# either give multiple identifier separated by comma (,) or put this option
|
||||||
|
# multiple time. See also the "--disable" option for examples.
|
||||||
|
|
||||||
|
|
||||||
|
enable=all
|
||||||
|
|
||||||
|
disable=C, # black is enforcing this for us already, incompatibly
|
||||||
|
W, # unbroaden this to the below specifics later on.
|
||||||
|
W0107, # uneccessary pass is stylisitc in most places
|
||||||
|
W0212, # Should likely refactor around protected access warnings later
|
||||||
|
W1203, # fstrings are too fast to care about enforcing this.
|
||||||
|
W0612, # unused vars can sometimes indicate an issue, but ...
|
||||||
|
W1401, # Should probably fix the reason this is disabled (start up screen)
|
||||||
|
W0511, # Nope, todos are fine for future people to see things to do.
|
||||||
|
W0613, # Too many places where we need to take unused args do to d.py ... also menus
|
||||||
|
W0221, # Overriden converters.
|
||||||
|
W0223, # abstractmethod not defined in mixins is expected
|
||||||
|
I, # ...
|
||||||
|
R # While some of these have merit, It's too large a burden to enable this right now.
|
||||||
|
|
||||||
|
|
||||||
|
[REPORTS]
|
||||||
|
|
||||||
|
output-format=parseable
|
||||||
|
files-output=no
|
||||||
|
reports=no
|
||||||
|
|
||||||
|
|
||||||
|
[LOGGING]
|
||||||
|
|
||||||
|
# Logging modules to check that the string format arguments are in logging
|
||||||
|
# function parameter format
|
||||||
|
logging-modules=logging
|
||||||
|
|
||||||
|
|
||||||
|
[TYPECHECK]
|
||||||
|
|
||||||
|
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||||
|
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||||
|
ignore-mixin-members=yes
|
||||||
|
|
||||||
|
# TODO: Write a plyint plugin to allow this with these mixin classes
|
||||||
|
# To use the abstractmethod we know will be defined in the final class.
|
||||||
|
ignored-classes=redbot.cogs.mod.movetocore.MoveToCore,
|
||||||
|
redbot.cogs.mod.kickban.KickBanMixin,
|
||||||
|
redbot.cogs.mod.mutes.MuteMixin,
|
||||||
|
redbot.cogs.mod.names.ModInfo,
|
||||||
|
redbot.cogs.mod.settings.ModSettings,
|
||||||
|
redbot.cogs.mod.events.Events
|
||||||
|
|
||||||
|
ignored-modules=distutils # https://github.com/PyCQA/pylint/issues/73
|
||||||
|
|
||||||
|
|
||||||
|
[VARIABLES]
|
||||||
|
|
||||||
|
# Tells whether we should check for unused import in __init__ files.
|
||||||
|
init-import=no
|
||||||
|
|
||||||
|
# A regular expression matching the name of dummy variables (i.e. expectedly
|
||||||
|
# not used).
|
||||||
|
dummy-variables-rgx=_$|dummy
|
||||||
|
|
||||||
|
|
||||||
|
[SIMILARITIES]
|
||||||
|
|
||||||
|
# Minimum lines number of a similarity.
|
||||||
|
min-similarity-lines=4
|
||||||
|
|
||||||
|
# Ignore comments when computing similarities.
|
||||||
|
ignore-comments=yes
|
||||||
|
|
||||||
|
# Ignore docstrings when computing similarities.
|
||||||
|
ignore-docstrings=yes
|
||||||
|
|
||||||
|
# Ignore imports when computing similarities.
|
||||||
|
ignore-imports=no
|
||||||
|
|
||||||
|
|
||||||
|
[MISCELLANEOUS]
|
||||||
|
|
||||||
|
# List of note tags to take in consideration, separated by a comma.
|
||||||
|
notes=FIXME,XXX,TODO
|
||||||
|
|
||||||
|
|
||||||
|
[CLASSES]
|
||||||
|
|
||||||
|
# List of method names used to declare (i.e. assign) instance attributes.
|
||||||
|
defining-attr-methods=__init__,__new__,__call__
|
||||||
|
|
||||||
|
# List of valid names for the first argument in a class method.
|
||||||
|
valid-classmethod-first-arg=cls
|
||||||
|
|
||||||
|
# List of valid names for the first argument in a metaclass class method.
|
||||||
|
valid-metaclass-classmethod-first-arg=mcs
|
||||||
|
|
||||||
|
# List of member names, which should be excluded from the protected access
|
||||||
|
# warning.
|
||||||
|
exclude-protected=
|
||||||
|
|
||||||
|
[EXCEPTIONS]
|
||||||
|
|
||||||
|
# Exceptions that will emit a warning when being caught. Defaults to
|
||||||
|
# "Exception"
|
||||||
|
overgeneral-exceptions=Exception,discord.DiscordException
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
formats:
|
formats:
|
||||||
- pdf
|
- pdf
|
||||||
|
|
||||||
@@ -5,8 +7,11 @@ build:
|
|||||||
image: latest
|
image: latest
|
||||||
|
|
||||||
python:
|
python:
|
||||||
version: 3.6
|
version: 3.7
|
||||||
pip_install: true
|
install:
|
||||||
extra_requirements:
|
- requirements: docs/requirements.txt
|
||||||
- docs
|
- method: pip
|
||||||
- mongo
|
path: .
|
||||||
|
extra_requirements:
|
||||||
|
- docs
|
||||||
|
- mongo
|
||||||
|
|||||||
27
.travis.yml
27
.travis.yml
@@ -3,16 +3,16 @@ language: python
|
|||||||
cache: pip
|
cache: pip
|
||||||
notifications:
|
notifications:
|
||||||
email: false
|
email: false
|
||||||
sudo: true
|
|
||||||
|
|
||||||
python:
|
python:
|
||||||
- 3.6.6
|
- 3.7.2
|
||||||
- 3.7
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
PIPENV_IGNORE_VIRTUALENVS=1
|
- PIPENV_IGNORE_VIRTUALENVS=1
|
||||||
matrix:
|
matrix:
|
||||||
TOXENV=py
|
- TOXENV=py
|
||||||
|
- TOXENV=docs
|
||||||
|
- TOXENV=style
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install --upgrade pip tox
|
- pip install --upgrade pip tox
|
||||||
@@ -22,32 +22,26 @@ script:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
|
|
||||||
- python: 3.6.6
|
|
||||||
env: TOXENV=docs
|
|
||||||
- python: 3.6.6
|
|
||||||
env: TOXENV=style
|
|
||||||
|
|
||||||
# These jobs only occur on tag creation if the prior ones succeed
|
# These jobs only occur on tag creation if the prior ones succeed
|
||||||
- stage: PyPi Deployment
|
- stage: PyPi Deployment
|
||||||
if: tag IS present
|
if: tag IS present
|
||||||
python: 3.6.6
|
python: 3.7.2
|
||||||
env:
|
env:
|
||||||
- DEPLOYING=true
|
- DEPLOYING=true
|
||||||
- TOXENV=py36
|
- TOXENV=py36
|
||||||
deploy:
|
deploy:
|
||||||
- provider: pypi
|
- provider: pypi
|
||||||
|
distributions: sdist bdist_wheel
|
||||||
user: Red-DiscordBot
|
user: Red-DiscordBot
|
||||||
password:
|
password:
|
||||||
secure: Ty9vYnd/wCuQkVC/OsS4E2jT9LVDVfzsFrQc4U2hMYcTJnYbl/3omyObdCWCOBC40vUDkVHAQU8ULHzoCA+2KX9Ds/7/P5zCumAA0uJRR9Smw7OlRzSMxJI+/lGq4CwXKzxDZKuo5rsxXEbW5qmYjtO8Mk6KuLkvieb1vyr2DcqWEFzg/7TZNDfD1oP8et8ITQ26lLP1dtQx/jlAiIBzgK9wziuwj1Divb9A///VsGz43N8maZ+jfsDjYqrfUVWTy3ar7JPUplletenYCR1PmQ5C46XfV0kitKd1aITJ48YPAKyYgKy8AIT+Uz1JArTnqdzLSFRNELS57qS00lzgllbteCyWQ8Uzy0Zpxb/5DDH8/mL1n0MyJrF8qjZd2hLNAXg3z/k9bGXeiMLGwoxRlGXkL2XpiVgI93UKKyVyooGNMgPTc/QdSc7krjAWcOtX/HgLR34jxeLPFEdzJNAFIimfDD8N+XTFcNBw6EvOYm/n5MXkckNoX/G+ThNobHZ7VKSASltZ9zBRAJ2dDh35G3CYmVEk33U77RKbL9le/Za9QVBcAO8i6rqVGYkdO7thHHKHc/1CB1jNnjsFSDt0bURtNfAqfwKCurQC8487zbEzT+2fog3Wygv7g3cklaRg4guY8UjZuFWStYGqbroTsOCd9ATNqeO5B13pNhllSzU=
|
secure: Ty9vYnd/wCuQkVC/OsS4E2jT9LVDVfzsFrQc4U2hMYcTJnYbl/3omyObdCWCOBC40vUDkVHAQU8ULHzoCA+2KX9Ds/7/P5zCumAA0uJRR9Smw7OlRzSMxJI+/lGq4CwXKzxDZKuo5rsxXEbW5qmYjtO8Mk6KuLkvieb1vyr2DcqWEFzg/7TZNDfD1oP8et8ITQ26lLP1dtQx/jlAiIBzgK9wziuwj1Divb9A///VsGz43N8maZ+jfsDjYqrfUVWTy3ar7JPUplletenYCR1PmQ5C46XfV0kitKd1aITJ48YPAKyYgKy8AIT+Uz1JArTnqdzLSFRNELS57qS00lzgllbteCyWQ8Uzy0Zpxb/5DDH8/mL1n0MyJrF8qjZd2hLNAXg3z/k9bGXeiMLGwoxRlGXkL2XpiVgI93UKKyVyooGNMgPTc/QdSc7krjAWcOtX/HgLR34jxeLPFEdzJNAFIimfDD8N+XTFcNBw6EvOYm/n5MXkckNoX/G+ThNobHZ7VKSASltZ9zBRAJ2dDh35G3CYmVEk33U77RKbL9le/Za9QVBcAO8i6rqVGYkdO7thHHKHc/1CB1jNnjsFSDt0bURtNfAqfwKCurQC8487zbEzT+2fog3Wygv7g3cklaRg4guY8UjZuFWStYGqbroTsOCd9ATNqeO5B13pNhllSzU=
|
||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
on:
|
on:
|
||||||
repo: Cog-Creators/Red-DiscordBot
|
repo: Cog-Creators/Red-DiscordBot
|
||||||
python: 3.6.6
|
|
||||||
tags: true
|
tags: true
|
||||||
- stage: Crowdin Deployment
|
- stage: Crowdin Deployment
|
||||||
if: tag IS present
|
if: tag IS present
|
||||||
python: 3.6.6
|
python: 3.7.2
|
||||||
env:
|
env:
|
||||||
- DEPLOYING=true
|
- DEPLOYING=true
|
||||||
- TOXENV=py36
|
- TOXENV=py36
|
||||||
@@ -56,12 +50,11 @@ jobs:
|
|||||||
- echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list
|
- echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list
|
||||||
- sudo apt-get update -qq
|
- sudo apt-get update -qq
|
||||||
- sudo apt-get install -y crowdin
|
- sudo apt-get install -y crowdin
|
||||||
- pip install redgettext==2.2
|
- pip install redgettext==3.1
|
||||||
deploy:
|
deploy:
|
||||||
- provider: script
|
- provider: script
|
||||||
script: make gettext
|
script: make upload_translations
|
||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
on:
|
on:
|
||||||
repo: Cog-Creators/Red-DiscordBot
|
repo: Cog-Creators/Red-DiscordBot
|
||||||
python: 3.6.6
|
|
||||||
tags: true
|
tags: true
|
||||||
|
|||||||
28
LICENSE
28
LICENSE
@@ -672,3 +672,31 @@ may consider it more useful to permit linking proprietary applications with
|
|||||||
the library. If this is what you want to do, use the GNU Lesser General
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
Public License instead of this License. But first, please read
|
Public License instead of this License. But first, please read
|
||||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||||
|
|
||||||
|
The Red-DiscordBot project contains subcomponents in audio.py that have a
|
||||||
|
separate copyright notice and license terms. Your use of the source code for
|
||||||
|
these subcomponents is subject to the terms and conditions of the following
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
This product bundles methods from https://github.com/Just-Some-Bots/MusicBot/
|
||||||
|
blob/master/musicbot/spotify.py which are available under an MIT license.
|
||||||
|
|
||||||
|
Copyright (c) 2015-2018 Just-Some-Bots (https://github.com/Just-Some-Bots)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
29
Makefile
29
Makefile
@@ -1,13 +1,26 @@
|
|||||||
|
# Python Code Style
|
||||||
reformat:
|
reformat:
|
||||||
black -l 99 -N `git ls-files "*.py"`
|
black -l 99 `git ls-files "*.py"`
|
||||||
stylecheck:
|
stylecheck:
|
||||||
black --check -l 99 -N `git ls-files "*.py"`
|
black --check -l 99 `git ls-files "*.py"`
|
||||||
|
|
||||||
|
# Translations
|
||||||
gettext:
|
gettext:
|
||||||
redgettext --command-docstrings --verbose --recursive redbot --exclude-files "redbot/pytest/**/*"
|
redgettext --command-docstrings --verbose --recursive redbot --exclude-files "redbot/pytest/**/*"
|
||||||
crowdin upload
|
upload_translations:
|
||||||
|
$(MAKE) gettext
|
||||||
|
crowdin upload sources
|
||||||
|
download_translations:
|
||||||
|
crowdin download
|
||||||
|
|
||||||
REF?=rewrite
|
# Dependencies
|
||||||
update_vendor:
|
bumpdeps:
|
||||||
pip install --upgrade --no-deps -t . https://github.com/Rapptz/discord.py/archive/$(REF).tar.gz#egg=discord.py
|
python tools/bumpdeps.py
|
||||||
rm -r discord.py*-info
|
|
||||||
$(MAKE) reformat
|
# Development environment
|
||||||
|
newenv:
|
||||||
|
python3.7 -m venv --clear .venv
|
||||||
|
.venv/bin/pip install -U pip setuptools
|
||||||
|
$(MAKE) syncenv
|
||||||
|
syncenv:
|
||||||
|
.venv/bin/pip install -Ur ./tools/dev-requirements.txt
|
||||||
|
|||||||
11
Pipfile
11
Pipfile
@@ -1,11 +0,0 @@
|
|||||||
[[source]]
|
|
||||||
url = "https://pypi.org/simple"
|
|
||||||
verify_ssl = true
|
|
||||||
name = "pypi"
|
|
||||||
|
|
||||||
[packages]
|
|
||||||
red-discordbot = {path = ".",editable = true,extras = ['mongo', 'voice']}
|
|
||||||
|
|
||||||
[dev-packages]
|
|
||||||
tox = "*"
|
|
||||||
red-discordbot = {path = ".",editable = true,extras = ['docs', 'test', 'style']}
|
|
||||||
841
Pipfile.lock
generated
841
Pipfile.lock
generated
@@ -1,841 +0,0 @@
|
|||||||
{
|
|
||||||
"_meta": {
|
|
||||||
"hash": {
|
|
||||||
"sha256": "b9f385e4c53c659dd76e8722d1fb69c244d3a76e4b0dfc40956ff2493277c1f6"
|
|
||||||
},
|
|
||||||
"pipfile-spec": 6,
|
|
||||||
"requires": {},
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"name": "pypi",
|
|
||||||
"url": "https://pypi.org/simple",
|
|
||||||
"verify_ssl": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"default": {
|
|
||||||
"aiohttp": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55",
|
|
||||||
"sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed",
|
|
||||||
"sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10",
|
|
||||||
"sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5",
|
|
||||||
"sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1",
|
|
||||||
"sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939",
|
|
||||||
"sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390",
|
|
||||||
"sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa",
|
|
||||||
"sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc",
|
|
||||||
"sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5",
|
|
||||||
"sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d",
|
|
||||||
"sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf",
|
|
||||||
"sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6",
|
|
||||||
"sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72",
|
|
||||||
"sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12",
|
|
||||||
"sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366",
|
|
||||||
"sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4",
|
|
||||||
"sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300",
|
|
||||||
"sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d",
|
|
||||||
"sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303",
|
|
||||||
"sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6",
|
|
||||||
"sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"
|
|
||||||
],
|
|
||||||
"version": "==3.5.4"
|
|
||||||
},
|
|
||||||
"aiohttp-json-rpc": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:1d040b7b10ff414f9174398ff6e9c647eb0434a00939450b33aa539177c51dcf",
|
|
||||||
"sha256:5f5fb141c6263d2ea52a4173babe9449eef4029620dc49936dca45cdc17ac9dd"
|
|
||||||
],
|
|
||||||
"version": "==0.12"
|
|
||||||
},
|
|
||||||
"appdirs": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
|
||||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
|
||||||
],
|
|
||||||
"version": "==1.4.3"
|
|
||||||
},
|
|
||||||
"async-timeout": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
|
||||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
|
||||||
],
|
|
||||||
"version": "==3.0.1"
|
|
||||||
},
|
|
||||||
"attrs": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
|
|
||||||
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
|
|
||||||
],
|
|
||||||
"version": "==18.2.0"
|
|
||||||
},
|
|
||||||
"chardet": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
|
||||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
|
||||||
],
|
|
||||||
"version": "==3.0.4"
|
|
||||||
},
|
|
||||||
"colorama": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
|
|
||||||
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
|
|
||||||
],
|
|
||||||
"version": "==0.4.1"
|
|
||||||
},
|
|
||||||
"distro": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57",
|
|
||||||
"sha256:eedf82a470ebe7d010f1872c17237c79ab04097948800029994fa458e52fb4b4"
|
|
||||||
],
|
|
||||||
"version": "==1.4.0"
|
|
||||||
},
|
|
||||||
"dnspython": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01",
|
|
||||||
"sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"
|
|
||||||
],
|
|
||||||
"version": "==1.16.0"
|
|
||||||
},
|
|
||||||
"fuzzywuzzy": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254",
|
|
||||||
"sha256:6f49de47db00e1c71d40ad16da42284ac357936fa9b66bea1df63fed07122d62"
|
|
||||||
],
|
|
||||||
"version": "==0.17.0"
|
|
||||||
},
|
|
||||||
"idna": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
|
||||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
|
||||||
],
|
|
||||||
"version": "==2.8"
|
|
||||||
},
|
|
||||||
"idna-ssl": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
|
|
||||||
],
|
|
||||||
"version": "==1.1.0"
|
|
||||||
},
|
|
||||||
"motor": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:462fbb824f4289481c158227a2579d6adaf1ec7c70cf7ebe60ed6ceb321e5869",
|
|
||||||
"sha256:d035c09ab422bc50bf3efb134f7405694cae76268545bd21e14fb22e2638f84e"
|
|
||||||
],
|
|
||||||
"version": "==2.0.0"
|
|
||||||
},
|
|
||||||
"multidict": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
|
|
||||||
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3",
|
|
||||||
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef",
|
|
||||||
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b",
|
|
||||||
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73",
|
|
||||||
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc",
|
|
||||||
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3",
|
|
||||||
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd",
|
|
||||||
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351",
|
|
||||||
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941",
|
|
||||||
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d",
|
|
||||||
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1",
|
|
||||||
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b",
|
|
||||||
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a",
|
|
||||||
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3",
|
|
||||||
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7",
|
|
||||||
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0",
|
|
||||||
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0",
|
|
||||||
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014",
|
|
||||||
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5",
|
|
||||||
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036",
|
|
||||||
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d",
|
|
||||||
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a",
|
|
||||||
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce",
|
|
||||||
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1",
|
|
||||||
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a",
|
|
||||||
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9",
|
|
||||||
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7",
|
|
||||||
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"
|
|
||||||
],
|
|
||||||
"version": "==4.5.2"
|
|
||||||
},
|
|
||||||
"pymongo": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:025f94fc1e1364f00e50badc88c47f98af20012f23317234e51a11333ef986e6",
|
|
||||||
"sha256:02aa7fb282606331aefbc0586e2cf540e9dbe5e343493295e7f390936ad2738e",
|
|
||||||
"sha256:057210e831573e932702cf332012ed39da78edf0f02d24a3f0b213264a87a397",
|
|
||||||
"sha256:0d946b79c56187fe139276d4c8ed612a27a616966c8b9779d6b79e2053587c8b",
|
|
||||||
"sha256:104790893b928d310aae8a955e0bdbaa442fb0ac0a33d1bbb0741c791a407778",
|
|
||||||
"sha256:15527ef218d95a8717486106553b0d54ff2641e795b65668754e17ab9ca6e381",
|
|
||||||
"sha256:1826527a0b032f6e20e7ac7f72d7c26dd476a5e5aa82c04aa1c7088a59fded7d",
|
|
||||||
"sha256:22e3aa4ce1c3eebc7f70f9ca7fd4ce1ea33e8bdb7b61996806cd312f08f84a3a",
|
|
||||||
"sha256:244e1101e9a48615b9a16cbd194f73c115fdfefc96894803158608115f703b26",
|
|
||||||
"sha256:24b8c04fdb633a84829d03909752c385faef249c06114cc8d8e1700b95aae5c8",
|
|
||||||
"sha256:2c276696350785d3104412cbe3ac70ab1e3a10c408e7b20599ee41403a3ed630",
|
|
||||||
"sha256:2d8474dc833b1182b651b184ace997a7bd83de0f51244de988d3c30e49f07de3",
|
|
||||||
"sha256:3119b57fe1d964781e91a53e81532c85ed1701baaddec592e22f6b77a9fdf3df",
|
|
||||||
"sha256:3bee8e7e0709b0fcdaa498a3e513bde9ffc7cd09dbceb11e425bd91c89dbd5b6",
|
|
||||||
"sha256:436c071e01a464753d30dbfc8768dd93aecf2a8e378e5314d130b95e77b4d612",
|
|
||||||
"sha256:46635e3f19ad04d5a7d7cf23d232388ddbfccf46d9a3b7436b6abadda4e84813",
|
|
||||||
"sha256:4772e0b679717e7ac4608d996f57b6f380748a919b457cb05bb941467b888b22",
|
|
||||||
"sha256:4e2cd80e16f481a62c3175b607373200e714ed29025f21559ebf7524f295689f",
|
|
||||||
"sha256:52732960efa0e003ca1c092dc0a3c65276e897681287a788a01ca78dda3b41f0",
|
|
||||||
"sha256:55a7de51ec7d1731b2431886d0349146645f2816e5b8eb982d7c49f89472c9f3",
|
|
||||||
"sha256:5f8ed5934197a2d4b2087646e98de3e099a237099dcf498b9e38dd3465f74ef4",
|
|
||||||
"sha256:64b064124fcbc8eb04a155117dc4d9a336e3cda3f069958fbc44fe70c3c3d1e9",
|
|
||||||
"sha256:65958b8e4319f992e85dad59d8081888b97fcdbde5f0d14bc28f2848b92d3ef1",
|
|
||||||
"sha256:7683428862e20c6a790c19e64f8ccf487f613fbc83d47e3d532df9c81668d451",
|
|
||||||
"sha256:78566d5570c75a127c2491e343dc006798a384f06be588fe9b0cbe5595711559",
|
|
||||||
"sha256:7d1cb00c093dbf1d0b16ccf123e79dee3b82608e4a2a88947695f0460eef13ff",
|
|
||||||
"sha256:8c74e2a9b594f7962c62cef7680a4cb92a96b4e6e3c2f970790da67cc0213a7e",
|
|
||||||
"sha256:8e60aa7699170f55f4b0f56ee6f8415229777ac7e4b4b1aa41fc61eec08c1f1d",
|
|
||||||
"sha256:9447b561529576d89d3bf973e5241a88cf76e45bd101963f5236888713dea774",
|
|
||||||
"sha256:970055bfeb0be373f2f5299a3db8432444bad3bc2f198753ee6c2a3a781e0959",
|
|
||||||
"sha256:a6344b8542e584e140dc3c651d68bde51270e79490aa9320f9e708f9b2c39bd5",
|
|
||||||
"sha256:ce309ca470d747b02ba6069d286a17b7df8e9c94d10d727d9cf3a64e51d85184",
|
|
||||||
"sha256:cfbd86ed4c2b2ac71bbdbcea6669bf295def7152e3722ddd9dda94ac7981f33d",
|
|
||||||
"sha256:d7929c513732dff093481f4a0954ed5ff16816365842136b17caa0b4992e49d3"
|
|
||||||
],
|
|
||||||
"version": "==3.7.2"
|
|
||||||
},
|
|
||||||
"python-levenshtein-wheels": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0065529c8aec4c044468286177761857d36981ba6f7fdb62d7d5f7ffd143de5d",
|
|
||||||
"sha256:016924a59d689f9f47d5f7b26b70f31e309255e8dd72602c91e93ceb752b9f92",
|
|
||||||
"sha256:089d046ea7727e583233c71fef1046663ed67b96967063ae8ddc9f551e86a4fc",
|
|
||||||
"sha256:0aea217eab612acd45dcc3424a2e8dbd977cc309f80359d0c01971f1e65b9a9b",
|
|
||||||
"sha256:0beb91ad80b1573829066e5af36b80190c367be6e0a65292f073353b0388c7fc",
|
|
||||||
"sha256:0fa2ca69ef803bc6037a8c919e2e8a17b55e94c9c9ffcb4c21befbb15a1d0f40",
|
|
||||||
"sha256:11c77d0d74ab7f46f89a58ae9c2d67349ebc1ae3e18636627f9939d810167c31",
|
|
||||||
"sha256:19a68716a322486ddffc8bf7e5cf44a82f7700b05a10658e6e7fc5c7ae92b13d",
|
|
||||||
"sha256:19a95a01d28d63b042438ba860c4ace90362906a038fa77962ba33325d377d10",
|
|
||||||
"sha256:1a61f3a51e00a3608659bbaabb3f27af37c9dbe84d843369061a3e45cf0d5103",
|
|
||||||
"sha256:1c50aebebab403fb2dd415d70355446ac364dece502b0e2737a1a085bb9a4aa4",
|
|
||||||
"sha256:1e51cdc123625a28709662d24ea0cb4cf6f991845e6054d9f803c78da1d6b08f",
|
|
||||||
"sha256:1f0056d3216b0fe38f25c6f8ebc84bd9f6d34c55a7a9414341b674fb98961399",
|
|
||||||
"sha256:228b59460e9a786e498bdfc8011838b89c6054650b115c86c9c819a055a793b0",
|
|
||||||
"sha256:23020f9ff2cb3457a926dcc470b84f9bd5b7646bd8b8e06b915bdbbc905cb23f",
|
|
||||||
"sha256:3e6bcca97a7ff4e720352b57ddc26380c0583dcdd4b791acef7b574ad58468a7",
|
|
||||||
"sha256:3ed88f9e638da57647149115c34e0e120cae6f3d35eee7d77e22cc9c1d8eced3",
|
|
||||||
"sha256:445bf7941cb1fa05d6c2a4a502ad4868a5cacd92e8eb77b2bd008cdda9d37c55",
|
|
||||||
"sha256:4ba5e147d76d7ee884fd6eae461438b080bcc9f2c6eb9b576811e1bcfe8f808e",
|
|
||||||
"sha256:4bb128b719c30f3b9feacfe71a338ae07d39dbffc077139416f3535c89f12362",
|
|
||||||
"sha256:53c0c9964390368fd64460b690f168221c669766b193b7e80ae3950c2b9551f8",
|
|
||||||
"sha256:57c4edef81611098d37176278f2b6a3712bf864eed313496d7d80504805896d1",
|
|
||||||
"sha256:7f7283dfe50eac8a8cd9b777de9eb50b1edf7dbb46fc7cc9d9b0050d0c135021",
|
|
||||||
"sha256:7f9759095b3fc825464a72b1cae95125e610eba3c70f91557754c32a0bf32ea2",
|
|
||||||
"sha256:98727050ba70eb8d318ec8a8203531c20119347fc8f281102b097326812742ab",
|
|
||||||
"sha256:ac9cdf044dcb9481c7da782db01b50c1f0e7cdd78c8507b963b6d072829c0263",
|
|
||||||
"sha256:b679f951f842c38665aa54bea4d7403099131f71fac6d8584f893a731fe1266d",
|
|
||||||
"sha256:b8c183dc4aa4e95dc5c373eedc3d205c176805835611fcfec5d9050736c695c4",
|
|
||||||
"sha256:c2c76f483d05eddec60a5cd89e92385adef565a4f243b1d9a6abe2f6bd2a7c0a",
|
|
||||||
"sha256:c388baa3c04272a7c585d3da24030c142353eb26eb531dd2681502e6be7d7a26",
|
|
||||||
"sha256:cb0f2a711db665b5bf8697b5af3b9884bb1139385c5c12c2e472e4bbee62da99",
|
|
||||||
"sha256:cbac984d7b36e75b440d1c8ff9d3425d778364a0cbc23f8943383d4decd35d5e",
|
|
||||||
"sha256:f9084ed3b8997ad4353d124b903f2860a9695b9e080663276d9e58c32e293244"
|
|
||||||
],
|
|
||||||
"version": "==0.13.1"
|
|
||||||
},
|
|
||||||
"pyyaml": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
|
|
||||||
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
|
|
||||||
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
|
|
||||||
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
|
|
||||||
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
|
|
||||||
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
|
|
||||||
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
|
|
||||||
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
|
|
||||||
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
|
|
||||||
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
|
|
||||||
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
|
|
||||||
],
|
|
||||||
"version": "==3.13"
|
|
||||||
},
|
|
||||||
"raven": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54",
|
|
||||||
"sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4"
|
|
||||||
],
|
|
||||||
"version": "==6.10.0"
|
|
||||||
},
|
|
||||||
"raven-aiohttp": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:1444a49c93a85b8bb57c6ee649e512368dce7a26ad64ac3a01d86aa5669d77f3",
|
|
||||||
"sha256:6a34b6a9841ad0fd827eeb158edb5826c5c5bd7babe2cde2a3f23eb85313af04"
|
|
||||||
],
|
|
||||||
"version": "==0.7.0"
|
|
||||||
},
|
|
||||||
"red-discordbot": {
|
|
||||||
"editable": true,
|
|
||||||
"extras": [
|
|
||||||
"mongo",
|
|
||||||
"voice"
|
|
||||||
],
|
|
||||||
"path": "."
|
|
||||||
},
|
|
||||||
"red-lavalink": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:13e1a3f91b990be9582cba039d9a32ec4cef760da1e7e6952143116ec83d4302",
|
|
||||||
"sha256:3dd0d73b4a908bbe9cfb703d2563dad1d1a58f8eea5896a0dacdf37d54a39d9c"
|
|
||||||
],
|
|
||||||
"version": "==0.2.3"
|
|
||||||
},
|
|
||||||
"schema": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687",
|
|
||||||
"sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74"
|
|
||||||
],
|
|
||||||
"version": "==0.6.8"
|
|
||||||
},
|
|
||||||
"websockets": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:04b42a1b57096ffa5627d6a78ea1ff7fad3bc2c0331ffc17bc32a4024da7fea0",
|
|
||||||
"sha256:08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f",
|
|
||||||
"sha256:10d89d4326045bf5e15e83e9867c85d686b612822e4d8f149cf4840aab5f46e0",
|
|
||||||
"sha256:232fac8a1978fc1dead4b1c2fa27c7756750fb393eb4ac52f6bc87ba7242b2fa",
|
|
||||||
"sha256:4bf4c8097440eff22bc78ec76fe2a865a6e658b6977a504679aaf08f02c121da",
|
|
||||||
"sha256:51642ea3a00772d1e48fb0c492f0d3ae3b6474f34d20eca005a83f8c9c06c561",
|
|
||||||
"sha256:55d86102282a636e195dad68aaaf85b81d0bef449d7e2ef2ff79ac450bb25d53",
|
|
||||||
"sha256:564d2675682bd497b59907d2205031acbf7d3fadf8c763b689b9ede20300b215",
|
|
||||||
"sha256:5d13bf5197a92149dc0badcc2b699267ff65a867029f465accfca8abab95f412",
|
|
||||||
"sha256:5eda665f6789edb9b57b57a159b9c55482cbe5b046d7db458948370554b16439",
|
|
||||||
"sha256:5edb2524d4032be4564c65dc4f9d01e79fe8fad5f966e5b552f4e5164fef0885",
|
|
||||||
"sha256:79691794288bc51e2a3b8de2bc0272ca8355d0b8503077ea57c0716e840ebaef",
|
|
||||||
"sha256:7fcc8681e9981b9b511cdee7c580d5b005f3bb86b65bde2188e04a29f1d63317",
|
|
||||||
"sha256:8e447e05ec88b1b408a4c9cde85aa6f4b04f06aa874b9f0b8e8319faf51b1fee",
|
|
||||||
"sha256:90ea6b3e7787620bb295a4ae050d2811c807d65b1486749414f78cfd6fb61489",
|
|
||||||
"sha256:9e13239952694b8b831088431d15f771beace10edfcf9ef230cefea14f18508f",
|
|
||||||
"sha256:d40f081187f7b54d7a99d8a5c782eaa4edc335a057aa54c85059272ed826dc09",
|
|
||||||
"sha256:e1df1a58ed2468c7b7ce9a2f9752a32ad08eac2bcd56318625c3647c2cd2da6f",
|
|
||||||
"sha256:e98d0cec437097f09c7834a11c69d79fe6241729b23f656cfc227e93294fc242",
|
|
||||||
"sha256:f8d59627702d2ff27cb495ca1abdea8bd8d581de425c56e93bff6517134e0a9b",
|
|
||||||
"sha256:fc30cdf2e949a2225b012a7911d1d031df3d23e99b7eda7dfc982dc4a860dae9"
|
|
||||||
],
|
|
||||||
"version": "==7.0"
|
|
||||||
},
|
|
||||||
"yarl": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9",
|
|
||||||
"sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f",
|
|
||||||
"sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb",
|
|
||||||
"sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320",
|
|
||||||
"sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842",
|
|
||||||
"sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0",
|
|
||||||
"sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829",
|
|
||||||
"sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310",
|
|
||||||
"sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4",
|
|
||||||
"sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8",
|
|
||||||
"sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"
|
|
||||||
],
|
|
||||||
"version": "==1.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"develop": {
|
|
||||||
"aiohttp": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55",
|
|
||||||
"sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed",
|
|
||||||
"sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10",
|
|
||||||
"sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5",
|
|
||||||
"sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1",
|
|
||||||
"sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939",
|
|
||||||
"sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390",
|
|
||||||
"sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa",
|
|
||||||
"sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc",
|
|
||||||
"sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5",
|
|
||||||
"sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d",
|
|
||||||
"sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf",
|
|
||||||
"sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6",
|
|
||||||
"sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72",
|
|
||||||
"sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12",
|
|
||||||
"sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366",
|
|
||||||
"sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4",
|
|
||||||
"sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300",
|
|
||||||
"sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d",
|
|
||||||
"sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303",
|
|
||||||
"sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6",
|
|
||||||
"sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"
|
|
||||||
],
|
|
||||||
"version": "==3.5.4"
|
|
||||||
},
|
|
||||||
"aiohttp-json-rpc": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:1d040b7b10ff414f9174398ff6e9c647eb0434a00939450b33aa539177c51dcf",
|
|
||||||
"sha256:5f5fb141c6263d2ea52a4173babe9449eef4029620dc49936dca45cdc17ac9dd"
|
|
||||||
],
|
|
||||||
"version": "==0.12"
|
|
||||||
},
|
|
||||||
"alabaster": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
|
|
||||||
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
|
|
||||||
],
|
|
||||||
"version": "==0.7.12"
|
|
||||||
},
|
|
||||||
"appdirs": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
|
||||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
|
||||||
],
|
|
||||||
"version": "==1.4.3"
|
|
||||||
},
|
|
||||||
"async-timeout": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
|
||||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
|
||||||
],
|
|
||||||
"version": "==3.0.1"
|
|
||||||
},
|
|
||||||
"atomicwrites": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
|
|
||||||
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
|
|
||||||
],
|
|
||||||
"version": "==1.3.0"
|
|
||||||
},
|
|
||||||
"attrs": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
|
|
||||||
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
|
|
||||||
],
|
|
||||||
"version": "==18.2.0"
|
|
||||||
},
|
|
||||||
"babel": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
|
|
||||||
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
|
|
||||||
],
|
|
||||||
"version": "==2.6.0"
|
|
||||||
},
|
|
||||||
"black": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739",
|
|
||||||
"sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"
|
|
||||||
],
|
|
||||||
"version": "==18.9b0"
|
|
||||||
},
|
|
||||||
"certifi": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
|
|
||||||
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
|
|
||||||
],
|
|
||||||
"version": "==2018.11.29"
|
|
||||||
},
|
|
||||||
"chardet": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
|
||||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
|
||||||
],
|
|
||||||
"version": "==3.0.4"
|
|
||||||
},
|
|
||||||
"click": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
|
||||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
|
||||||
],
|
|
||||||
"version": "==7.0"
|
|
||||||
},
|
|
||||||
"colorama": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
|
|
||||||
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
|
|
||||||
],
|
|
||||||
"version": "==0.4.1"
|
|
||||||
},
|
|
||||||
"distro": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57",
|
|
||||||
"sha256:eedf82a470ebe7d010f1872c17237c79ab04097948800029994fa458e52fb4b4"
|
|
||||||
],
|
|
||||||
"version": "==1.4.0"
|
|
||||||
},
|
|
||||||
"docutils": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
|
|
||||||
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
|
|
||||||
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
|
|
||||||
],
|
|
||||||
"version": "==0.14"
|
|
||||||
},
|
|
||||||
"filelock": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633",
|
|
||||||
"sha256:d610c1bb404daf85976d7a82eb2ada120f04671007266b708606565dd03b5be6"
|
|
||||||
],
|
|
||||||
"version": "==3.0.10"
|
|
||||||
},
|
|
||||||
"fuzzywuzzy": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254",
|
|
||||||
"sha256:6f49de47db00e1c71d40ad16da42284ac357936fa9b66bea1df63fed07122d62"
|
|
||||||
],
|
|
||||||
"version": "==0.17.0"
|
|
||||||
},
|
|
||||||
"idna": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
|
||||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
|
||||||
],
|
|
||||||
"version": "==2.8"
|
|
||||||
},
|
|
||||||
"idna-ssl": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
|
|
||||||
],
|
|
||||||
"version": "==1.1.0"
|
|
||||||
},
|
|
||||||
"imagesize": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
|
|
||||||
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
|
|
||||||
],
|
|
||||||
"version": "==1.1.0"
|
|
||||||
},
|
|
||||||
"jinja2": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
|
||||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
|
||||||
],
|
|
||||||
"version": "==2.10"
|
|
||||||
},
|
|
||||||
"markupsafe": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
|
|
||||||
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
|
|
||||||
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
|
|
||||||
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
|
|
||||||
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
|
|
||||||
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
|
|
||||||
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
|
|
||||||
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
|
|
||||||
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
|
|
||||||
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
|
|
||||||
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
|
|
||||||
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
|
|
||||||
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
|
|
||||||
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
|
|
||||||
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
|
|
||||||
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
|
|
||||||
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
|
|
||||||
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
|
|
||||||
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
|
|
||||||
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
|
|
||||||
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
|
|
||||||
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
|
|
||||||
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
|
|
||||||
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
|
|
||||||
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
|
|
||||||
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
|
|
||||||
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
|
|
||||||
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
|
|
||||||
],
|
|
||||||
"version": "==1.1.0"
|
|
||||||
},
|
|
||||||
"more-itertools": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
|
|
||||||
"sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
|
|
||||||
],
|
|
||||||
"version": "==6.0.0"
|
|
||||||
},
|
|
||||||
"multidict": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
|
|
||||||
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3",
|
|
||||||
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef",
|
|
||||||
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b",
|
|
||||||
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73",
|
|
||||||
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc",
|
|
||||||
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3",
|
|
||||||
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd",
|
|
||||||
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351",
|
|
||||||
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941",
|
|
||||||
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d",
|
|
||||||
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1",
|
|
||||||
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b",
|
|
||||||
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a",
|
|
||||||
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3",
|
|
||||||
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7",
|
|
||||||
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0",
|
|
||||||
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0",
|
|
||||||
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014",
|
|
||||||
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5",
|
|
||||||
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036",
|
|
||||||
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d",
|
|
||||||
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a",
|
|
||||||
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce",
|
|
||||||
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1",
|
|
||||||
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a",
|
|
||||||
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9",
|
|
||||||
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7",
|
|
||||||
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"
|
|
||||||
],
|
|
||||||
"version": "==4.5.2"
|
|
||||||
},
|
|
||||||
"packaging": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af",
|
|
||||||
"sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"
|
|
||||||
],
|
|
||||||
"version": "==19.0"
|
|
||||||
},
|
|
||||||
"pluggy": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616",
|
|
||||||
"sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"
|
|
||||||
],
|
|
||||||
"version": "==0.8.1"
|
|
||||||
},
|
|
||||||
"py": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
|
|
||||||
"sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
|
|
||||||
],
|
|
||||||
"version": "==1.7.0"
|
|
||||||
},
|
|
||||||
"pygments": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a",
|
|
||||||
"sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"
|
|
||||||
],
|
|
||||||
"version": "==2.3.1"
|
|
||||||
},
|
|
||||||
"pyparsing": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a",
|
|
||||||
"sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3"
|
|
||||||
],
|
|
||||||
"version": "==2.3.1"
|
|
||||||
},
|
|
||||||
"pytest": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:65aeaa77ae87c7fc95de56285282546cfa9c886dc8e5dc78313db1c25e21bc07",
|
|
||||||
"sha256:6ac6d467d9f053e95aaacd79f831dbecfe730f419c6c7022cb316b365cd9199d"
|
|
||||||
],
|
|
||||||
"version": "==4.2.0"
|
|
||||||
},
|
|
||||||
"pytest-asyncio": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf",
|
|
||||||
"sha256:d734718e25cfc32d2bf78d346e99d33724deeba774cc4afdf491530c6184b63b"
|
|
||||||
],
|
|
||||||
"version": "==0.10.0"
|
|
||||||
},
|
|
||||||
"python-levenshtein-wheels": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0065529c8aec4c044468286177761857d36981ba6f7fdb62d7d5f7ffd143de5d",
|
|
||||||
"sha256:016924a59d689f9f47d5f7b26b70f31e309255e8dd72602c91e93ceb752b9f92",
|
|
||||||
"sha256:089d046ea7727e583233c71fef1046663ed67b96967063ae8ddc9f551e86a4fc",
|
|
||||||
"sha256:0aea217eab612acd45dcc3424a2e8dbd977cc309f80359d0c01971f1e65b9a9b",
|
|
||||||
"sha256:0beb91ad80b1573829066e5af36b80190c367be6e0a65292f073353b0388c7fc",
|
|
||||||
"sha256:0fa2ca69ef803bc6037a8c919e2e8a17b55e94c9c9ffcb4c21befbb15a1d0f40",
|
|
||||||
"sha256:11c77d0d74ab7f46f89a58ae9c2d67349ebc1ae3e18636627f9939d810167c31",
|
|
||||||
"sha256:19a68716a322486ddffc8bf7e5cf44a82f7700b05a10658e6e7fc5c7ae92b13d",
|
|
||||||
"sha256:19a95a01d28d63b042438ba860c4ace90362906a038fa77962ba33325d377d10",
|
|
||||||
"sha256:1a61f3a51e00a3608659bbaabb3f27af37c9dbe84d843369061a3e45cf0d5103",
|
|
||||||
"sha256:1c50aebebab403fb2dd415d70355446ac364dece502b0e2737a1a085bb9a4aa4",
|
|
||||||
"sha256:1e51cdc123625a28709662d24ea0cb4cf6f991845e6054d9f803c78da1d6b08f",
|
|
||||||
"sha256:1f0056d3216b0fe38f25c6f8ebc84bd9f6d34c55a7a9414341b674fb98961399",
|
|
||||||
"sha256:228b59460e9a786e498bdfc8011838b89c6054650b115c86c9c819a055a793b0",
|
|
||||||
"sha256:23020f9ff2cb3457a926dcc470b84f9bd5b7646bd8b8e06b915bdbbc905cb23f",
|
|
||||||
"sha256:3e6bcca97a7ff4e720352b57ddc26380c0583dcdd4b791acef7b574ad58468a7",
|
|
||||||
"sha256:3ed88f9e638da57647149115c34e0e120cae6f3d35eee7d77e22cc9c1d8eced3",
|
|
||||||
"sha256:445bf7941cb1fa05d6c2a4a502ad4868a5cacd92e8eb77b2bd008cdda9d37c55",
|
|
||||||
"sha256:4ba5e147d76d7ee884fd6eae461438b080bcc9f2c6eb9b576811e1bcfe8f808e",
|
|
||||||
"sha256:4bb128b719c30f3b9feacfe71a338ae07d39dbffc077139416f3535c89f12362",
|
|
||||||
"sha256:53c0c9964390368fd64460b690f168221c669766b193b7e80ae3950c2b9551f8",
|
|
||||||
"sha256:57c4edef81611098d37176278f2b6a3712bf864eed313496d7d80504805896d1",
|
|
||||||
"sha256:7f7283dfe50eac8a8cd9b777de9eb50b1edf7dbb46fc7cc9d9b0050d0c135021",
|
|
||||||
"sha256:7f9759095b3fc825464a72b1cae95125e610eba3c70f91557754c32a0bf32ea2",
|
|
||||||
"sha256:98727050ba70eb8d318ec8a8203531c20119347fc8f281102b097326812742ab",
|
|
||||||
"sha256:ac9cdf044dcb9481c7da782db01b50c1f0e7cdd78c8507b963b6d072829c0263",
|
|
||||||
"sha256:b679f951f842c38665aa54bea4d7403099131f71fac6d8584f893a731fe1266d",
|
|
||||||
"sha256:b8c183dc4aa4e95dc5c373eedc3d205c176805835611fcfec5d9050736c695c4",
|
|
||||||
"sha256:c2c76f483d05eddec60a5cd89e92385adef565a4f243b1d9a6abe2f6bd2a7c0a",
|
|
||||||
"sha256:c388baa3c04272a7c585d3da24030c142353eb26eb531dd2681502e6be7d7a26",
|
|
||||||
"sha256:cb0f2a711db665b5bf8697b5af3b9884bb1139385c5c12c2e472e4bbee62da99",
|
|
||||||
"sha256:cbac984d7b36e75b440d1c8ff9d3425d778364a0cbc23f8943383d4decd35d5e",
|
|
||||||
"sha256:f9084ed3b8997ad4353d124b903f2860a9695b9e080663276d9e58c32e293244"
|
|
||||||
],
|
|
||||||
"version": "==0.13.1"
|
|
||||||
},
|
|
||||||
"pytz": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
|
|
||||||
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
|
|
||||||
],
|
|
||||||
"version": "==2018.9"
|
|
||||||
},
|
|
||||||
"pyyaml": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
|
|
||||||
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
|
|
||||||
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
|
|
||||||
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
|
|
||||||
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
|
|
||||||
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
|
|
||||||
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
|
|
||||||
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
|
|
||||||
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
|
|
||||||
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
|
|
||||||
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
|
|
||||||
],
|
|
||||||
"version": "==3.13"
|
|
||||||
},
|
|
||||||
"raven": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54",
|
|
||||||
"sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4"
|
|
||||||
],
|
|
||||||
"version": "==6.10.0"
|
|
||||||
},
|
|
||||||
"raven-aiohttp": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:1444a49c93a85b8bb57c6ee649e512368dce7a26ad64ac3a01d86aa5669d77f3",
|
|
||||||
"sha256:6a34b6a9841ad0fd827eeb158edb5826c5c5bd7babe2cde2a3f23eb85313af04"
|
|
||||||
],
|
|
||||||
"version": "==0.7.0"
|
|
||||||
},
|
|
||||||
"red-discordbot": {
|
|
||||||
"editable": true,
|
|
||||||
"extras": [
|
|
||||||
"mongo",
|
|
||||||
"voice"
|
|
||||||
],
|
|
||||||
"path": "."
|
|
||||||
},
|
|
||||||
"red-lavalink": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:13e1a3f91b990be9582cba039d9a32ec4cef760da1e7e6952143116ec83d4302",
|
|
||||||
"sha256:3dd0d73b4a908bbe9cfb703d2563dad1d1a58f8eea5896a0dacdf37d54a39d9c"
|
|
||||||
],
|
|
||||||
"version": "==0.2.3"
|
|
||||||
},
|
|
||||||
"requests": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
|
|
||||||
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
|
|
||||||
],
|
|
||||||
"version": "==2.21.0"
|
|
||||||
},
|
|
||||||
"schema": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687",
|
|
||||||
"sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74"
|
|
||||||
],
|
|
||||||
"version": "==0.6.8"
|
|
||||||
},
|
|
||||||
"six": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
|
||||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
|
||||||
],
|
|
||||||
"version": "==1.12.0"
|
|
||||||
},
|
|
||||||
"snowballstemmer": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
|
|
||||||
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
|
|
||||||
],
|
|
||||||
"version": "==1.2.1"
|
|
||||||
},
|
|
||||||
"sphinx": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:b53904fa7cb4b06a39409a492b949193a1b68cc7241a1a8ce9974f86f0d24287",
|
|
||||||
"sha256:c1c00fc4f6e8b101a0d037065043460dffc2d507257f2f11acaed71fd2b0c83c"
|
|
||||||
],
|
|
||||||
"version": "==1.8.4"
|
|
||||||
},
|
|
||||||
"sphinx-rtd-theme": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4",
|
|
||||||
"sha256:728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a"
|
|
||||||
],
|
|
||||||
"version": "==0.4.3"
|
|
||||||
},
|
|
||||||
"sphinxcontrib-asyncio": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:96627b1ec4eba08d09ad577ff9416c131910333ef37a2c82a2716e59646739f0"
|
|
||||||
],
|
|
||||||
"version": "==0.2.0"
|
|
||||||
},
|
|
||||||
"sphinxcontrib-websupport": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
|
|
||||||
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
|
|
||||||
],
|
|
||||||
"version": "==1.1.0"
|
|
||||||
},
|
|
||||||
"toml": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
|
||||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
|
||||||
],
|
|
||||||
"version": "==0.10.0"
|
|
||||||
},
|
|
||||||
"tox": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:04f8f1aa05de8e76d7a266ccd14e0d665d429977cd42123bc38efa9b59964e9e",
|
|
||||||
"sha256:25ef928babe88c71e3ed3af0c464d1160b01fca2dd1870a5bb26c2dea61a17fc"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==3.7.0"
|
|
||||||
},
|
|
||||||
"urllib3": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
|
|
||||||
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
|
|
||||||
],
|
|
||||||
"version": "==1.24.1"
|
|
||||||
},
|
|
||||||
"virtualenv": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:8b9abfc51c38b70f61634bf265e5beacf6fae11fc25d355d1871f49b8e45f0db",
|
|
||||||
"sha256:cceab52aa7d4df1e1871a70236eb2b89fcfe29b6b43510d9738689787c513261"
|
|
||||||
],
|
|
||||||
"version": "==16.4.0"
|
|
||||||
},
|
|
||||||
"websockets": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:04b42a1b57096ffa5627d6a78ea1ff7fad3bc2c0331ffc17bc32a4024da7fea0",
|
|
||||||
"sha256:08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f",
|
|
||||||
"sha256:10d89d4326045bf5e15e83e9867c85d686b612822e4d8f149cf4840aab5f46e0",
|
|
||||||
"sha256:232fac8a1978fc1dead4b1c2fa27c7756750fb393eb4ac52f6bc87ba7242b2fa",
|
|
||||||
"sha256:4bf4c8097440eff22bc78ec76fe2a865a6e658b6977a504679aaf08f02c121da",
|
|
||||||
"sha256:51642ea3a00772d1e48fb0c492f0d3ae3b6474f34d20eca005a83f8c9c06c561",
|
|
||||||
"sha256:55d86102282a636e195dad68aaaf85b81d0bef449d7e2ef2ff79ac450bb25d53",
|
|
||||||
"sha256:564d2675682bd497b59907d2205031acbf7d3fadf8c763b689b9ede20300b215",
|
|
||||||
"sha256:5d13bf5197a92149dc0badcc2b699267ff65a867029f465accfca8abab95f412",
|
|
||||||
"sha256:5eda665f6789edb9b57b57a159b9c55482cbe5b046d7db458948370554b16439",
|
|
||||||
"sha256:5edb2524d4032be4564c65dc4f9d01e79fe8fad5f966e5b552f4e5164fef0885",
|
|
||||||
"sha256:79691794288bc51e2a3b8de2bc0272ca8355d0b8503077ea57c0716e840ebaef",
|
|
||||||
"sha256:7fcc8681e9981b9b511cdee7c580d5b005f3bb86b65bde2188e04a29f1d63317",
|
|
||||||
"sha256:8e447e05ec88b1b408a4c9cde85aa6f4b04f06aa874b9f0b8e8319faf51b1fee",
|
|
||||||
"sha256:90ea6b3e7787620bb295a4ae050d2811c807d65b1486749414f78cfd6fb61489",
|
|
||||||
"sha256:9e13239952694b8b831088431d15f771beace10edfcf9ef230cefea14f18508f",
|
|
||||||
"sha256:d40f081187f7b54d7a99d8a5c782eaa4edc335a057aa54c85059272ed826dc09",
|
|
||||||
"sha256:e1df1a58ed2468c7b7ce9a2f9752a32ad08eac2bcd56318625c3647c2cd2da6f",
|
|
||||||
"sha256:e98d0cec437097f09c7834a11c69d79fe6241729b23f656cfc227e93294fc242",
|
|
||||||
"sha256:f8d59627702d2ff27cb495ca1abdea8bd8d581de425c56e93bff6517134e0a9b",
|
|
||||||
"sha256:fc30cdf2e949a2225b012a7911d1d031df3d23e99b7eda7dfc982dc4a860dae9"
|
|
||||||
],
|
|
||||||
"version": "==7.0"
|
|
||||||
},
|
|
||||||
"yarl": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9",
|
|
||||||
"sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f",
|
|
||||||
"sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb",
|
|
||||||
"sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320",
|
|
||||||
"sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842",
|
|
||||||
"sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0",
|
|
||||||
"sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829",
|
|
||||||
"sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310",
|
|
||||||
"sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4",
|
|
||||||
"sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8",
|
|
||||||
"sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"
|
|
||||||
],
|
|
||||||
"version": "==1.3.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
45
README.md
45
README.md
@@ -16,21 +16,21 @@
|
|||||||
<img src="https://img.shields.io/badge/Support-Red!-yellow.svg" alt="Support Red on Patreon!">
|
<img src="https://img.shields.io/badge/Support-Red!-yellow.svg" alt="Support Red on Patreon!">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.python.org/downloads/">
|
<a href="https://www.python.org/downloads/">
|
||||||
<img src="https://img.shields.io/badge/Made%20With-Python%203-blue.svg?style=for-the-badge" alt="Made with Python 3">
|
<img src="https://img.shields.io/badge/Made%20With-Python%203.7-blue.svg?style=for-the-badge" alt="Made with Python 3.7">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://crowdin.com/project/red-discordbot">
|
<a href="https://crowdin.com/project/red-discordbot">
|
||||||
<img src="https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg" alt="Localized with Crowdin">
|
<img src="https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg" alt="Localized with Crowdin">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Rapptz/discord.py/tree/rewrite">
|
<a href="https://github.com/Rapptz/discord.py/">
|
||||||
<img src="https://img.shields.io/badge/discord-py-blue.svg" alt="discord.py">
|
<img src="https://img.shields.io/badge/discord-py-blue.svg" alt="discord.py">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://travis-ci.org/Cog-Creators/Red-DiscordBot">
|
<a href="https://travis-ci.com/Cog-Creators/Red-DiscordBot">
|
||||||
<img src="https://api.travis-ci.org/Cog-Creators/Red-DiscordBot.svg?branch=V3/develop" alt="Travis CI">
|
<img src="https://api.travis-ci.com/Cog-Creators/Red-DiscordBot.svg?branch=V3/develop" alt="Travis CI">
|
||||||
</a>
|
</a>
|
||||||
<a href="http://red-discordbot.readthedocs.io/en/v3-develop/?badge=v3-develop">
|
<a href="http://red-discordbot.readthedocs.io/en/stable/?badge=stable">
|
||||||
<img src="https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop" alt="Red on readthedocs.org">
|
<img src="https://readthedocs.org/projects/red-discordbot/badge/?version=stable" alt="Red on readthedocs.org">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/ambv/black">
|
<a href="https://github.com/ambv/black">
|
||||||
<img src="https://img.shields.io/badge/code%20style-black-000000.svg" alt="Code Style: Black">
|
<img src="https://img.shields.io/badge/code%20style-black-000000.svg" alt="Code Style: Black">
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
•
|
•
|
||||||
<a href="#installation">Installation</a>
|
<a href="#installation">Installation</a>
|
||||||
•
|
•
|
||||||
<a href="http://red-discordbot.readthedocs.io/en/v3-develop/index.html">Documentation</a>
|
<a href="http://red-discordbot.readthedocs.io/en/stable/index.html">Documentation</a>
|
||||||
•
|
•
|
||||||
<a href="#plugins">Plugins</a>
|
<a href="#plugins">Plugins</a>
|
||||||
•
|
•
|
||||||
@@ -83,19 +83,17 @@ community of cog repositories.**
|
|||||||
|
|
||||||
**The following platforms are officially supported:**
|
**The following platforms are officially supported:**
|
||||||
|
|
||||||
- [Windows](https://red-discordbot.readthedocs.io/en/v3-develop/install_windows.html)
|
- [Windows](https://red-discordbot.readthedocs.io/en/stable/install_windows.html)
|
||||||
- [MacOS](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
- [MacOS](https://red-discordbot.readthedocs.io/en/stable/install_linux_mac.html)
|
||||||
- [Ubuntu](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
- [Ubuntu](https://red-discordbot.readthedocs.io/en/stable/install_linux_mac.html)
|
||||||
- [Debian Stretch](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
- [Debian Stretch](https://red-discordbot.readthedocs.io/en/stable/install_linux_mac.html)
|
||||||
- [CentOS 7](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
- [CentOS 7](https://red-discordbot.readthedocs.io/en/stable/install_linux_mac.html)
|
||||||
- [Arch Linux](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
- [Arch Linux](https://red-discordbot.readthedocs.io/en/stable/install_linux_mac.html)
|
||||||
- [Raspbian Stretch](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
|
- [Raspbian Stretch](https://red-discordbot.readthedocs.io/en/stable/install_linux_mac.html)
|
||||||
|
|
||||||
Already using **Red** V2? Take a look at the [Data Converter](https://red-discordbot.readthedocs.io/en/v3-develop/cog_dataconverter.html)
|
|
||||||
to import your data to V3.
|
|
||||||
|
|
||||||
If after reading the guide you are still experiencing issues, feel free to join the
|
If after reading the guide you are still experiencing issues, feel free to join the
|
||||||
[Official Discord Server](https://discord.gg/red) and ask in the **#v3-support** channel for help.
|
[Official Discord Server](https://discord.gg/red) and ask in the **#support** channel for help.
|
||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
|
|
||||||
@@ -108,18 +106,18 @@ plugins directly from Discord! A few examples are:
|
|||||||
- Casino
|
- Casino
|
||||||
- Reaction roles
|
- Reaction roles
|
||||||
- Slow Mode
|
- Slow Mode
|
||||||
- Anilist
|
- AniList
|
||||||
- And much, much more!
|
- And much, much more!
|
||||||
|
|
||||||
Feel free to take a [peek](https://github.com/Cog-Creators/Red-DiscordBot/issues/1398) at a list of
|
Feel free to take a [peek](https://cogboard.red/t/approved-repositories/210) at a list of
|
||||||
available 3rd party cogs!
|
available 3rd party cogs!
|
||||||
|
|
||||||
# Join the community!
|
# Join the community!
|
||||||
|
|
||||||
**Red** is in continuous development, and it’s supported by an active community which produces new
|
**Red** is in continuous development, and it’s supported by an active community which produces new
|
||||||
content (cogs/plugins) for everyone to enjoy. New features are constantly added. If you can’t
|
content (cogs/plugins) for everyone to enjoy. New features are constantly added. If you can’t
|
||||||
[find](https://github.com/Cog-Creators/Red-DiscordBot/issues/1398) the cog you’re looking for,
|
[find](https://cogboard.red/t/approved-repositories/210) the cog you’re looking for,
|
||||||
consult our [guide](https://red-discordbot.readthedocs.io/en/v3-develop/guide_cog_creation.html) on
|
consult our [guide](https://red-discordbot.readthedocs.io/en/stable/guide_cog_creation.html) on
|
||||||
building your own cogs!
|
building your own cogs!
|
||||||
|
|
||||||
Join us on our [Official Discord Server](https://discord.gg/red)!
|
Join us on our [Official Discord Server](https://discord.gg/red)!
|
||||||
@@ -128,11 +126,6 @@ Join us on our [Official Discord Server](https://discord.gg/red)!
|
|||||||
|
|
||||||
Released under the [GNU GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html) license.
|
Released under the [GNU GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html) license.
|
||||||
|
|
||||||
This project vendors the
|
|
||||||
[discord.py library by Rapptz](https://github.com/Rapptz/discord.py/tree/rewrite), which is
|
|
||||||
licensed under the [MIT License](https://opensource.org/licenses/MIT). This amounts to everything
|
|
||||||
within the *discord* folder of this repository.
|
|
||||||
|
|
||||||
Red is named after the main character of "Transistor", a video game by
|
Red is named after the main character of "Transistor", a video game by
|
||||||
[Super Giant Games](https://www.supergiantgames.com/games/transistor/).
|
[Super Giant Games](https://www.supergiantgames.com/games/transistor/).
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
api_key_env: CROWDIN_API_KEY
|
api_key_env: CROWDIN_API_KEY
|
||||||
project_identifier_env: CROWDIN_PROJECT_ID
|
project_identifier_env: CROWDIN_PROJECT_ID
|
||||||
|
base_path: ./redbot/
|
||||||
files:
|
files:
|
||||||
- source: /redbot/**/*.pot
|
- source: cogs/**/messages.pot
|
||||||
|
translation: /%original_path%/%locale%.po
|
||||||
|
- source: core/**/messages.pot
|
||||||
translation: /%original_path%/%locale%.po
|
translation: /%original_path%/%locale%.po
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
Discord API Wrapper
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
A basic wrapper for the Discord API.
|
|
||||||
|
|
||||||
:copyright: (c) 2015-2019 Rapptz
|
|
||||||
:license: MIT, see LICENSE for more details.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
__title__ = "discord"
|
|
||||||
__author__ = "Rapptz"
|
|
||||||
__license__ = "MIT"
|
|
||||||
__copyright__ = "Copyright 2015-2019 Rapptz"
|
|
||||||
__version__ = "1.0.0a"
|
|
||||||
|
|
||||||
from collections import namedtuple
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from .client import Client, AppInfo
|
|
||||||
from .user import User, ClientUser, Profile
|
|
||||||
from .emoji import Emoji, PartialEmoji
|
|
||||||
from .activity import *
|
|
||||||
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, abc
|
|
||||||
from .enums import *
|
|
||||||
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
|
|
||||||
from .raw_models import *
|
|
||||||
|
|
||||||
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())
|
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 argparse
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import discord
|
|
||||||
|
|
||||||
|
|
||||||
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 exc:
|
|
||||||
print('Could not load extension {{0}} due to {{1.__class__.__name__}}: {{1}}'.format(cog, exc))
|
|
||||||
|
|
||||||
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 exc:
|
|
||||||
parser.error("could not create our bot directory ({})".format(exc))
|
|
||||||
|
|
||||||
cogs = new_directory / "cogs"
|
|
||||||
|
|
||||||
try:
|
|
||||||
cogs.mkdir(exist_ok=True)
|
|
||||||
init = cogs / "__init__.py"
|
|
||||||
init.touch()
|
|
||||||
except OSError as exc:
|
|
||||||
print("warning: could not create cogs directory ({})".format(exc))
|
|
||||||
|
|
||||||
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 exc:
|
|
||||||
parser.error("could not create config file ({})".format(exc))
|
|
||||||
|
|
||||||
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 exc:
|
|
||||||
parser.error("could not create bot file ({})".format(exc))
|
|
||||||
|
|
||||||
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 exc:
|
|
||||||
print("warning: could not create .gitignore file ({})".format(exc))
|
|
||||||
|
|
||||||
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 exc:
|
|
||||||
print("warning: could not create cogs directory ({})".format(exc))
|
|
||||||
|
|
||||||
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 exc:
|
|
||||||
parser.error("could not create cog file ({})".format(exc))
|
|
||||||
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()
|
|
||||||
1030
discord/abc.py
1030
discord/abc.py
File diff suppressed because it is too large
Load Diff
@@ -1,613 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 .enums import ActivityType, try_enum
|
|
||||||
from .colour import Colour
|
|
||||||
|
|
||||||
__all__ = ["Activity", "Streaming", "Game", "Spotify"]
|
|
||||||
|
|
||||||
"""If curious, this is the current schema for an activity.
|
|
||||||
|
|
||||||
It's fairly long so I will document it here:
|
|
||||||
|
|
||||||
All keys are optional.
|
|
||||||
|
|
||||||
state: str (max: 128),
|
|
||||||
details: str (max: 128)
|
|
||||||
timestamps: dict
|
|
||||||
start: int (min: 1)
|
|
||||||
end: int (min: 1)
|
|
||||||
assets: dict
|
|
||||||
large_image: str (max: 32)
|
|
||||||
large_text: str (max: 128)
|
|
||||||
small_image: str (max: 32)
|
|
||||||
small_text: str (max: 128)
|
|
||||||
party: dict
|
|
||||||
id: str (max: 128),
|
|
||||||
size: List[int] (max-length: 2)
|
|
||||||
elem: int (min: 1)
|
|
||||||
secrets: dict
|
|
||||||
match: str (max: 128)
|
|
||||||
join: str (max: 128)
|
|
||||||
spectate: str (max: 128)
|
|
||||||
instance: bool
|
|
||||||
application_id: str
|
|
||||||
name: str (max: 128)
|
|
||||||
url: str
|
|
||||||
type: int
|
|
||||||
sync_id: str
|
|
||||||
session_id: str
|
|
||||||
flags: int
|
|
||||||
|
|
||||||
There are also activity flags which are mostly uninteresting for the library atm.
|
|
||||||
|
|
||||||
t.ActivityFlags = {
|
|
||||||
INSTANCE: 1,
|
|
||||||
JOIN: 2,
|
|
||||||
SPECTATE: 4,
|
|
||||||
JOIN_REQUEST: 8,
|
|
||||||
SYNC: 16,
|
|
||||||
PLAY: 32
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class _ActivityTag:
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
|
|
||||||
class Activity(_ActivityTag):
|
|
||||||
"""Represents an activity in Discord.
|
|
||||||
|
|
||||||
This could be an activity such as streaming, playing, listening
|
|
||||||
or watching.
|
|
||||||
|
|
||||||
For memory optimisation purposes, some activities are offered in slimmed
|
|
||||||
down versions:
|
|
||||||
|
|
||||||
- :class:`Game`
|
|
||||||
- :class:`Streaming`
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
------------
|
|
||||||
application_id: :class:`str`
|
|
||||||
The application ID of the game.
|
|
||||||
name: :class:`str`
|
|
||||||
The name of the activity.
|
|
||||||
url: :class:`str`
|
|
||||||
A stream URL that the activity could be doing.
|
|
||||||
type: :class:`ActivityType`
|
|
||||||
The type of activity currently being done.
|
|
||||||
state: :class:`str`
|
|
||||||
The user's current state. For example, "In Game".
|
|
||||||
details: :class:`str`
|
|
||||||
The detail of the user's current activity.
|
|
||||||
timestamps: :class:`dict`
|
|
||||||
A dictionary of timestamps. It contains the following optional keys:
|
|
||||||
|
|
||||||
- ``start``: Corresponds to when the user started doing the
|
|
||||||
activity in milliseconds since Unix epoch.
|
|
||||||
- ``end``: Corresponds to when the user will finish doing the
|
|
||||||
activity in milliseconds since Unix epoch.
|
|
||||||
|
|
||||||
assets: :class:`dict`
|
|
||||||
A dictionary representing the images and their hover text of an activity.
|
|
||||||
It contains the following optional keys:
|
|
||||||
|
|
||||||
- ``large_image``: A string representing the ID for the large image asset.
|
|
||||||
- ``large_text``: A string representing the text when hovering over the large image asset.
|
|
||||||
- ``small_image``: A string representing the ID for the small image asset.
|
|
||||||
- ``small_text``: A string representing the text when hovering over the small image asset.
|
|
||||||
|
|
||||||
party: :class:`dict`
|
|
||||||
A dictionary representing the activity party. It contains the following optional keys:
|
|
||||||
|
|
||||||
- ``id``: A string representing the party ID.
|
|
||||||
- ``size``: A list of up to two integer elements denoting (current_size, maximum_size).
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = (
|
|
||||||
"state",
|
|
||||||
"details",
|
|
||||||
"timestamps",
|
|
||||||
"assets",
|
|
||||||
"party",
|
|
||||||
"flags",
|
|
||||||
"sync_id",
|
|
||||||
"session_id",
|
|
||||||
"type",
|
|
||||||
"name",
|
|
||||||
"url",
|
|
||||||
"application_id",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
self.state = kwargs.pop("state", None)
|
|
||||||
self.details = kwargs.pop("details", None)
|
|
||||||
self.timestamps = kwargs.pop("timestamps", {})
|
|
||||||
self.assets = kwargs.pop("assets", {})
|
|
||||||
self.party = kwargs.pop("party", {})
|
|
||||||
self.application_id = kwargs.pop("application_id", None)
|
|
||||||
self.name = kwargs.pop("name", None)
|
|
||||||
self.url = kwargs.pop("url", None)
|
|
||||||
self.flags = kwargs.pop("flags", 0)
|
|
||||||
self.sync_id = kwargs.pop("sync_id", None)
|
|
||||||
self.session_id = kwargs.pop("session_id", None)
|
|
||||||
self.type = try_enum(ActivityType, kwargs.pop("type", -1))
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
ret = {}
|
|
||||||
for attr in self.__slots__:
|
|
||||||
value = getattr(self, attr, None)
|
|
||||||
if value is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if isinstance(value, dict) and len(value) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
ret[attr] = value
|
|
||||||
ret["type"] = int(self.type)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
@property
|
|
||||||
def start(self):
|
|
||||||
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable."""
|
|
||||||
try:
|
|
||||||
return datetime.datetime.utcfromtimestamp(self.timestamps["start"] / 1000)
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def end(self):
|
|
||||||
"""Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable."""
|
|
||||||
try:
|
|
||||||
return datetime.datetime.utcfromtimestamp(self.timestamps["end"] / 1000)
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def large_image_url(self):
|
|
||||||
"""Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity if applicable."""
|
|
||||||
if self.application_id is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
large_image = self.assets["large_image"]
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return "https://cdn.discordapp.com/app-assets/{0}/{1}.png".format(
|
|
||||||
self.application_id, large_image
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def small_image_url(self):
|
|
||||||
"""Optional[:class:`str`]: Returns a URL pointing to the small image asset of this activity if applicable."""
|
|
||||||
if self.application_id is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
small_image = self.assets["small_image"]
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return "https://cdn.discordapp.com/app-assets/{0}/{1}.png".format(
|
|
||||||
self.application_id, small_image
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def large_image_text(self):
|
|
||||||
"""Optional[:class:`str`]: Returns the large image asset hover text of this activity if applicable."""
|
|
||||||
return self.assets.get("large_text", None)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def small_image_text(self):
|
|
||||||
"""Optional[:class:`str`]: Returns the small image asset hover text of this activity if applicable."""
|
|
||||||
return self.assets.get("small_text", None)
|
|
||||||
|
|
||||||
|
|
||||||
class Game(_ActivityTag):
|
|
||||||
"""A slimmed down version of :class:`Activity` that represents a Discord game.
|
|
||||||
|
|
||||||
This is typically displayed via **Playing** on the official Discord client.
|
|
||||||
|
|
||||||
.. 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.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
-----------
|
|
||||||
name: :class:`str`
|
|
||||||
The game's name.
|
|
||||||
start: Optional[:class:`datetime.datetime`]
|
|
||||||
A naive UTC timestamp representing when the game started. Keyword-only parameter. Ignored for bots.
|
|
||||||
end: Optional[:class:`datetime.datetime`]
|
|
||||||
A naive UTC timestamp representing when the game ends. Keyword-only parameter. Ignored for bots.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
-----------
|
|
||||||
name: :class:`str`
|
|
||||||
The game's name.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("name", "_end", "_start")
|
|
||||||
|
|
||||||
def __init__(self, name, **extra):
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
try:
|
|
||||||
timestamps = extra["timestamps"]
|
|
||||||
except KeyError:
|
|
||||||
self._extract_timestamp(extra, "start")
|
|
||||||
self._extract_timestamp(extra, "end")
|
|
||||||
else:
|
|
||||||
self._start = timestamps.get("start", 0)
|
|
||||||
self._end = timestamps.get("end", 0)
|
|
||||||
|
|
||||||
def _extract_timestamp(self, data, key):
|
|
||||||
try:
|
|
||||||
dt = data[key]
|
|
||||||
except KeyError:
|
|
||||||
setattr(self, "_" + key, 0)
|
|
||||||
else:
|
|
||||||
setattr(self, "_" + key, dt.timestamp() * 1000.0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def type(self):
|
|
||||||
"""Returns the game's type. This is for compatibility with :class:`Activity`.
|
|
||||||
|
|
||||||
It always returns :attr:`ActivityType.playing`.
|
|
||||||
"""
|
|
||||||
return ActivityType.playing
|
|
||||||
|
|
||||||
@property
|
|
||||||
def start(self):
|
|
||||||
"""Optional[:class:`datetime.datetime`]: When the user started playing this game in UTC, if applicable."""
|
|
||||||
if self._start:
|
|
||||||
return datetime.datetime.utcfromtimestamp(self._start / 1000)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def end(self):
|
|
||||||
"""Optional[:class:`datetime.datetime`]: When the user will stop playing this game in UTC, if applicable."""
|
|
||||||
if self._end:
|
|
||||||
return datetime.datetime.utcfromtimestamp(self._end / 1000)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.name)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Game name={0.name!r}>".format(self)
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
timestamps = {}
|
|
||||||
if self._start:
|
|
||||||
timestamps["start"] = self._start
|
|
||||||
|
|
||||||
if self._end:
|
|
||||||
timestamps["end"] = self._end
|
|
||||||
|
|
||||||
return {
|
|
||||||
"type": ActivityType.playing.value,
|
|
||||||
"name": str(self.name),
|
|
||||||
"timestamps": timestamps,
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class Streaming(_ActivityTag):
|
|
||||||
"""A slimmed down version of :class:`Activity` that represents a Discord streaming status.
|
|
||||||
|
|
||||||
This is typically displayed via **Streaming** on the official Discord client.
|
|
||||||
|
|
||||||
.. container:: operations
|
|
||||||
|
|
||||||
.. describe:: x == y
|
|
||||||
|
|
||||||
Checks if two streams are equal.
|
|
||||||
|
|
||||||
.. describe:: x != y
|
|
||||||
|
|
||||||
Checks if two streams are not equal.
|
|
||||||
|
|
||||||
.. describe:: hash(x)
|
|
||||||
|
|
||||||
Returns the stream's hash.
|
|
||||||
|
|
||||||
.. describe:: str(x)
|
|
||||||
|
|
||||||
Returns the stream's name.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
-----------
|
|
||||||
name: :class:`str`
|
|
||||||
The stream's name.
|
|
||||||
url: :class:`str`
|
|
||||||
The stream's URL. Currently only twitch.tv URLs are supported. Anything else is silently
|
|
||||||
discarded.
|
|
||||||
details: Optional[:class:`str`]
|
|
||||||
If provided, typically the game the streamer is playing.
|
|
||||||
assets: :class:`dict`
|
|
||||||
A dictionary comprising of similar keys than those in :attr:`Activity.assets`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("name", "url", "details", "assets")
|
|
||||||
|
|
||||||
def __init__(self, *, name, url, **extra):
|
|
||||||
self.name = name
|
|
||||||
self.url = url
|
|
||||||
self.details = extra.pop("details", None)
|
|
||||||
self.assets = extra.pop("assets", {})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def type(self):
|
|
||||||
"""Returns the game's type. This is for compatibility with :class:`Activity`.
|
|
||||||
|
|
||||||
It always returns :attr:`ActivityType.streaming`.
|
|
||||||
"""
|
|
||||||
return ActivityType.streaming
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.name)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Streaming name={0.name!r}>".format(self)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def twitch_name(self):
|
|
||||||
"""Optional[:class:`str`]: If provided, the twitch name of the user streaming.
|
|
||||||
|
|
||||||
This corresponds to the ``large_image`` key of the :attr:`Streaming.assets`
|
|
||||||
dictionary if it starts with ``twitch:``. Typically set by the Discord client.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
name = self.assets["large_image"]
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return name[7:] if name[:7] == "twitch:" else None
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
ret = {
|
|
||||||
"type": ActivityType.streaming.value,
|
|
||||||
"name": str(self.name),
|
|
||||||
"url": str(self.url),
|
|
||||||
"assets": self.assets,
|
|
||||||
}
|
|
||||||
if self.details:
|
|
||||||
ret["details"] = self.details
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return isinstance(other, Streaming) and other.name == self.name and other.url == self.url
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not self.__eq__(other)
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(self.name)
|
|
||||||
|
|
||||||
|
|
||||||
class Spotify:
|
|
||||||
"""Represents a Spotify listening activity from Discord. This is a special case of
|
|
||||||
:class:`Activity` that makes it easier to work with the Spotify integration.
|
|
||||||
|
|
||||||
.. container:: operations
|
|
||||||
|
|
||||||
.. describe:: x == y
|
|
||||||
|
|
||||||
Checks if two activities are equal.
|
|
||||||
|
|
||||||
.. describe:: x != y
|
|
||||||
|
|
||||||
Checks if two activities are not equal.
|
|
||||||
|
|
||||||
.. describe:: hash(x)
|
|
||||||
|
|
||||||
Returns the activity's hash.
|
|
||||||
|
|
||||||
.. describe:: str(x)
|
|
||||||
|
|
||||||
Returns the string 'Spotify'.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = (
|
|
||||||
"_state",
|
|
||||||
"_details",
|
|
||||||
"_timestamps",
|
|
||||||
"_assets",
|
|
||||||
"_party",
|
|
||||||
"_sync_id",
|
|
||||||
"_session_id",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, **data):
|
|
||||||
self._state = data.pop("state", None)
|
|
||||||
self._details = data.pop("details", None)
|
|
||||||
self._timestamps = data.pop("timestamps", {})
|
|
||||||
self._assets = data.pop("assets", {})
|
|
||||||
self._party = data.pop("party", {})
|
|
||||||
self._sync_id = data.pop("sync_id")
|
|
||||||
self._session_id = data.pop("session_id")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def type(self):
|
|
||||||
"""Returns the activity's type. This is for compatibility with :class:`Activity`.
|
|
||||||
|
|
||||||
It always returns :attr:`ActivityType.listening`.
|
|
||||||
"""
|
|
||||||
return ActivityType.listening
|
|
||||||
|
|
||||||
@property
|
|
||||||
def colour(self):
|
|
||||||
"""Returns the Spotify integration colour, as a :class:`Colour`.
|
|
||||||
|
|
||||||
There is an alias for this named :meth:`color`"""
|
|
||||||
return Colour(0x1DB954)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def color(self):
|
|
||||||
"""Returns the Spotify integration colour, as a :class:`Colour`.
|
|
||||||
|
|
||||||
There is an alias for this named :meth:`colour`"""
|
|
||||||
return self.colour
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
"flags": 48, # SYNC | PLAY
|
|
||||||
"name": "Spotify",
|
|
||||||
"assets": self._assets,
|
|
||||||
"party": self._party,
|
|
||||||
"sync_id": self._sync_id,
|
|
||||||
"session_id": self._session_id,
|
|
||||||
"timestamps": self._timestamps,
|
|
||||||
"details": self._details,
|
|
||||||
"state": self._state,
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
""":class:`str`: The activity's name. This will always return "Spotify"."""
|
|
||||||
return "Spotify"
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return isinstance(other, Spotify) and other._session_id == self._session_id
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not self.__eq__(other)
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(self._session_id)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Spotify"
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Spotify title={0.title!r} artist={0.artist!r} track_id={0.track_id!r}>".format(
|
|
||||||
self
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def title(self):
|
|
||||||
""":class:`str`: The title of the song being played."""
|
|
||||||
return self._details
|
|
||||||
|
|
||||||
@property
|
|
||||||
def artists(self):
|
|
||||||
"""List[:class:`str`]: The artists of the song being played."""
|
|
||||||
return self._state.split("; ")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def artist(self):
|
|
||||||
""":class:`str`: The artist of the song being played.
|
|
||||||
|
|
||||||
This does not attempt to split the artist information into
|
|
||||||
multiple artists. Useful if there's only a single artist.
|
|
||||||
"""
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def album(self):
|
|
||||||
""":class:`str`: The album that the song being played belongs to."""
|
|
||||||
return self._assets.get("large_text", "")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def album_cover_url(self):
|
|
||||||
""":class:`str`: The album cover image URL from Spotify's CDN."""
|
|
||||||
large_image = self._assets.get("large_image", "")
|
|
||||||
if large_image[:8] != "spotify:":
|
|
||||||
return ""
|
|
||||||
album_image_id = large_image[8:]
|
|
||||||
return "https://i.scdn.co/image/" + album_image_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def track_id(self):
|
|
||||||
""":class:`str`: The track ID used by Spotify to identify this song."""
|
|
||||||
return self._sync_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def start(self):
|
|
||||||
""":class:`datetime.datetime`: When the user started playing this song in UTC."""
|
|
||||||
return datetime.datetime.utcfromtimestamp(self._timestamps["start"] / 1000)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def end(self):
|
|
||||||
""":class:`datetime.datetime`: When the user will stop playing this song in UTC."""
|
|
||||||
return datetime.datetime.utcfromtimestamp(self._timestamps["end"] / 1000)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def duration(self):
|
|
||||||
""":class:`datetime.timedelta`: The duration of the song being played."""
|
|
||||||
return self.end - self.start
|
|
||||||
|
|
||||||
@property
|
|
||||||
def party_id(self):
|
|
||||||
""":class:`str`: The party ID of the listening party."""
|
|
||||||
return self._party.get("id", "")
|
|
||||||
|
|
||||||
|
|
||||||
def create_activity(data):
|
|
||||||
if not data:
|
|
||||||
return None
|
|
||||||
|
|
||||||
game_type = try_enum(ActivityType, data.get("type", -1))
|
|
||||||
if game_type is ActivityType.playing:
|
|
||||||
if "application_id" in data or "session_id" in data:
|
|
||||||
return Activity(**data)
|
|
||||||
return Game(**data)
|
|
||||||
elif game_type is ActivityType.streaming:
|
|
||||||
if "url" in data:
|
|
||||||
return Streaming(**data)
|
|
||||||
return Activity(**data)
|
|
||||||
elif game_type is ActivityType.listening and "sync_id" in data and "session_id" in data:
|
|
||||||
return Spotify(**data)
|
|
||||||
return Activity(**data)
|
|
||||||
@@ -1,366 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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_default_notifications(entry, data):
|
|
||||||
return enums.try_enum(enums.NotificationLevel, 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 = entry.guild.get_role(ow_id)
|
|
||||||
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),
|
|
||||||
"rate_limit_per_user": ("slowmode_delay", None),
|
|
||||||
"default_message_notifications": (
|
|
||||||
"default_notifications",
|
|
||||||
_transform_default_notifications,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
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):
|
|
||||||
if not hasattr(first, "roles"):
|
|
||||||
setattr(first, "roles", [])
|
|
||||||
|
|
||||||
data = []
|
|
||||||
g = entry.guild
|
|
||||||
|
|
||||||
for e in elem:
|
|
||||||
role_id = int(e["id"])
|
|
||||||
role = g.get_role(role_id)
|
|
||||||
|
|
||||||
if role is None:
|
|
||||||
role = Object(id=role_id)
|
|
||||||
role.name = e["name"]
|
|
||||||
|
|
||||||
data.append(role)
|
|
||||||
|
|
||||||
setattr(second, "roles", data)
|
|
||||||
|
|
||||||
|
|
||||||
class AuditLogEntry:
|
|
||||||
r"""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: :class:`int`
|
|
||||||
The entry ID.
|
|
||||||
target: Any
|
|
||||||
The target that got changed. The exact type of this depends on
|
|
||||||
the action being done.
|
|
||||||
reason: Optional[:class:`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 = self.guild.get_role(instance_id)
|
|
||||||
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 = self.guild.get_role(target_id)
|
|
||||||
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)
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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)
|
|
||||||
157
discord/calls.py
157
discord/calls.py
@@ -1,157 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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):
|
|
||||||
""":obj:`bool`: Indicates if the call has ended."""
|
|
||||||
return self.ended_timestamp is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def channel(self):
|
|
||||||
r""":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.created_at
|
|
||||||
else:
|
|
||||||
return self.ended_timestamp - self.message.created_at
|
|
||||||
|
|
||||||
|
|
||||||
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: :obj:`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(lookup.get, 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 :obj:`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):
|
|
||||||
r""":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)
|
|
||||||
1008
discord/channel.py
1008
discord/channel.py
File diff suppressed because it is too large
Load Diff
1074
discord/client.py
1074
discord/client.py
File diff suppressed because it is too large
Load Diff
@@ -1,234 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 colorsys
|
|
||||||
|
|
||||||
|
|
||||||
class Colour:
|
|
||||||
"""Represents a Discord role colour. This class is similar
|
|
||||||
to an (red, green, blue) :class:`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: :class:`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 from_hsv(cls, h, s, v):
|
|
||||||
"""Constructs a :class:`Colour` from an HSV tuple."""
|
|
||||||
rgb = colorsys.hsv_to_rgb(h, s, v)
|
|
||||||
return cls.from_rgb(*(int(x * 255) for x in rgb))
|
|
||||||
|
|
||||||
@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
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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
|
|
||||||
|
|
||||||
|
|
||||||
def _typing_done_callback(fut):
|
|
||||||
# just retrieve any exception and call it a day
|
|
||||||
try:
|
|
||||||
fut.exception()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Typing:
|
|
||||||
def __init__(self, messageable):
|
|
||||||
self.loop = messageable._state.loop
|
|
||||||
self.messageable = messageable
|
|
||||||
|
|
||||||
async def do_typing(self):
|
|
||||||
try:
|
|
||||||
channel = self._channel
|
|
||||||
except AttributeError:
|
|
||||||
channel = await self.messageable._get_channel()
|
|
||||||
|
|
||||||
typing = channel._state.http.send_typing
|
|
||||||
|
|
||||||
while True:
|
|
||||||
await typing(channel.id)
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self.task = asyncio.ensure_future(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()
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
|
||||||
self._channel = channel = await self.messageable._get_channel()
|
|
||||||
await channel._state.http.send_typing(channel.id)
|
|
||||||
return self.__enter__()
|
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc, tb):
|
|
||||||
self.task.cancel()
|
|
||||||
@@ -1,492 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 :class:`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 :class:`str` are implicitly
|
|
||||||
casted to :class:`str` for you.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
-----------
|
|
||||||
title: :class:`str`
|
|
||||||
The title of the embed.
|
|
||||||
type: :class:`str`
|
|
||||||
The type of embed. Usually "rich".
|
|
||||||
description: :class:`str`
|
|
||||||
The description of the embed.
|
|
||||||
url: :class:`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 :class:`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 :class:`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
|
|
||||||
279
discord/emoji.py
279
discord/emoji.py
@@ -1,279 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 collections import namedtuple
|
|
||||||
|
|
||||||
from . import utils
|
|
||||||
from .mixins import Hashable
|
|
||||||
|
|
||||||
|
|
||||||
class PartialEmoji(namedtuple("PartialEmoji", "animated name id")):
|
|
||||||
"""Represents a "partial" 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: :class:`str`
|
|
||||||
The custom emoji name, if applicable, or the unicode codepoint
|
|
||||||
of the non-custom emoji.
|
|
||||||
animated: :class:`bool`
|
|
||||||
Whether the emoji is animated or not.
|
|
||||||
id: Optional[:class:`int`]
|
|
||||||
The ID of the custom emoji, if applicable.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.id is None:
|
|
||||||
return self.name
|
|
||||||
if self.animated:
|
|
||||||
return "<a:%s:%s>" % (self.name, self.id)
|
|
||||||
return "<:%s:%s>" % (self.name, self.id)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if self.is_unicode_emoji():
|
|
||||||
return isinstance(other, PartialEmoji) and self.name == other.name
|
|
||||||
|
|
||||||
if isinstance(other, (PartialEmoji, Emoji)):
|
|
||||||
return self.id == other.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)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url(self):
|
|
||||||
"""Returns a URL version of the emoji, if it is custom."""
|
|
||||||
if self.is_unicode_emoji():
|
|
||||||
return None
|
|
||||||
|
|
||||||
_format = "gif" if self.animated else "png"
|
|
||||||
return "https://cdn.discordapp.com/emojis/{0.id}.{1}".format(self, _format)
|
|
||||||
|
|
||||||
|
|
||||||
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: :class:`str`
|
|
||||||
The name of the emoji.
|
|
||||||
id: :class:`int`
|
|
||||||
The emoji's ID.
|
|
||||||
require_colons: :class:`bool`
|
|
||||||
If colons are required to use this emoji in the client (:PJSalt: vs PJSalt).
|
|
||||||
animated: :class:`bool`
|
|
||||||
Whether an emoji is animated or not.
|
|
||||||
managed: :class:`bool`
|
|
||||||
If this emoji is managed by a Twitch integration.
|
|
||||||
guild_id: :class:`int`
|
|
||||||
The guild ID the emoji belongs to.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = (
|
|
||||||
"require_colons",
|
|
||||||
"animated",
|
|
||||||
"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.animated = emoji.get("animated", False)
|
|
||||||
self._roles = utils.SnowflakeList(map(int, 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):
|
|
||||||
if self.animated:
|
|
||||||
return "<a:{0.name}:{0.id}>".format(self)
|
|
||||||
return "<:{0.name}:{0.id}>".format(self)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Emoji id={0.id} name={0.name!r}>".format(self)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return isinstance(other, (PartialEmoji, Emoji)) and self.id == other.id
|
|
||||||
|
|
||||||
@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."""
|
|
||||||
_format = "gif" if self.animated else "png"
|
|
||||||
return "https://cdn.discordapp.com/emojis/{0.id}.{1}".format(self, _format)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def roles(self):
|
|
||||||
"""List[:class:`Role`]: A :class:`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 self._roles.has(role.id)]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def guild(self):
|
|
||||||
""":class:`Guild`: The guild this emoji belongs to."""
|
|
||||||
return self._state._get_guild(self.guild_id)
|
|
||||||
|
|
||||||
async def delete(self, *, reason=None):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Deletes the custom emoji.
|
|
||||||
|
|
||||||
You must have :attr:`~Permissions.manage_emojis` permission to
|
|
||||||
do this.
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
await self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason)
|
|
||||||
|
|
||||||
async def edit(self, *, name, roles=None, reason=None):
|
|
||||||
r"""|coro|
|
|
||||||
|
|
||||||
Edits the custom emoji.
|
|
||||||
|
|
||||||
You must have :attr:`~Permissions.manage_emojis` permission to
|
|
||||||
do this.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
-----------
|
|
||||||
name: str
|
|
||||||
The new emoji name.
|
|
||||||
roles: Optional[list[:class:`Role`]]
|
|
||||||
A :class:`list` of :class:`Role`\s that can use this emoji. Leave empty to make it available to everyone.
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if roles:
|
|
||||||
roles = [role.id for role in roles]
|
|
||||||
await self._state.http.edit_custom_emoji(
|
|
||||||
self.guild.id, self.id, name=name, roles=roles, reason=reason
|
|
||||||
)
|
|
||||||
285
discord/enums.py
285
discord/enums.py
@@ -1,285 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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",
|
|
||||||
"SpeakingState",
|
|
||||||
"VerificationLevel",
|
|
||||||
"ContentFilter",
|
|
||||||
"Status",
|
|
||||||
"DefaultAvatar",
|
|
||||||
"RelationshipType",
|
|
||||||
"AuditLogAction",
|
|
||||||
"AuditLogActionCategory",
|
|
||||||
"UserFlags",
|
|
||||||
"ActivityType",
|
|
||||||
"HypeSquadHouse",
|
|
||||||
"NotificationLevel",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
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"
|
|
||||||
hongkong = "hongkong"
|
|
||||||
russia = "russia"
|
|
||||||
japan = "japan"
|
|
||||||
southafrica = "southafrica"
|
|
||||||
vip_us_east = "vip-us-east"
|
|
||||||
vip_us_west = "vip-us-west"
|
|
||||||
vip_amsterdam = "vip-amsterdam"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
|
|
||||||
class SpeakingState(IntEnum):
|
|
||||||
none = 0
|
|
||||||
voice = 1
|
|
||||||
soundshare = 2
|
|
||||||
priority = 4
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
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 NotificationLevel(IntEnum):
|
|
||||||
all_messages = 0
|
|
||||||
only_mentions = 1
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
bug_hunter = 8
|
|
||||||
hypesquad_bravery = 64
|
|
||||||
hypesquad_brilliance = 128
|
|
||||||
hypesquad_balance = 256
|
|
||||||
early_supporter = 512
|
|
||||||
|
|
||||||
|
|
||||||
class ActivityType(IntEnum):
|
|
||||||
unknown = -1
|
|
||||||
playing = 0
|
|
||||||
streaming = 1
|
|
||||||
listening = 2
|
|
||||||
watching = 3
|
|
||||||
|
|
||||||
|
|
||||||
class HypeSquadHouse(Enum):
|
|
||||||
bravery = 1
|
|
||||||
brilliance = 2
|
|
||||||
balance = 3
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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."""
|
|
||||||
|
|
||||||
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 KeyError:
|
|
||||||
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: :class:`str`
|
|
||||||
The text of the error. Could be an empty string.
|
|
||||||
status: :class:`int`
|
|
||||||
The status code of the HTTP request.
|
|
||||||
code: :class:`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: :class:`int`
|
|
||||||
The close code of the websocket.
|
|
||||||
reason: :class:`str`
|
|
||||||
The reason provided for the closure.
|
|
||||||
shard_id: Optional[:class:`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))
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
discord.ext.commands
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
An extension module to facilitate creation of bot commands.
|
|
||||||
|
|
||||||
:copyright: (c) 2019 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 *
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,225 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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.abc
|
|
||||||
import discord.utils
|
|
||||||
|
|
||||||
|
|
||||||
class Context(discord.abc.Messageable):
|
|
||||||
r"""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: :class:`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: :class:`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: :class:`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: :class:`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[:class:`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: :class:`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
|
|
||||||
|
|
||||||
async def invoke(self, *args, **kwargs):
|
|
||||||
r"""|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 = await command.callback(*arguments, **kwargs)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
async 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:
|
|
||||||
await 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
|
|
||||||
|
|
||||||
async 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):
|
|
||||||
r"""Optional[:class:`VoiceClient`]: A shortcut to :attr:`Guild.voice_client`\, if applicable."""
|
|
||||||
g = self.guild
|
|
||||||
return g.voice_client if g else None
|
|
||||||
@@ -1,560 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 re
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
import discord
|
|
||||||
|
|
||||||
from .errors import BadArgument, NoPrivateMessage
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"Converter",
|
|
||||||
"MemberConverter",
|
|
||||||
"UserConverter",
|
|
||||||
"TextChannelConverter",
|
|
||||||
"InviteConverter",
|
|
||||||
"RoleConverter",
|
|
||||||
"GameConverter",
|
|
||||||
"ColourConverter",
|
|
||||||
"VoiceChannelConverter",
|
|
||||||
"EmojiConverter",
|
|
||||||
"PartialEmojiConverter",
|
|
||||||
"CategoryChannelConverter",
|
|
||||||
"IDConverter",
|
|
||||||
"clean_content",
|
|
||||||
"Greedy",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async 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
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def convert(self, ctx, argument):
|
|
||||||
bot = ctx.bot
|
|
||||||
match = self._get_id_match(argument) or re.match(r"<@!?([0-9]+)>$", argument)
|
|
||||||
guild = ctx.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
|
|
||||||
"""
|
|
||||||
|
|
||||||
async 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
|
|
||||||
"""
|
|
||||||
|
|
||||||
async 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
|
|
||||||
"""
|
|
||||||
|
|
||||||
async 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
|
|
||||||
"""
|
|
||||||
|
|
||||||
async 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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async 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
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def convert(self, ctx, argument):
|
|
||||||
guild = ctx.guild
|
|
||||||
if not guild:
|
|
||||||
raise NoPrivateMessage()
|
|
||||||
|
|
||||||
match = self._get_id_match(argument) or re.match(r"<@&([0-9]+)>$", argument)
|
|
||||||
if match:
|
|
||||||
result = guild.get_role(int(match.group(1)))
|
|
||||||
else:
|
|
||||||
result = discord.utils.get(guild._roles.values(), name=argument)
|
|
||||||
|
|
||||||
if result is None:
|
|
||||||
raise BadArgument('Role "{}" not found.'.format(argument))
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class GameConverter(Converter):
|
|
||||||
"""Converts to :class:`Game`."""
|
|
||||||
|
|
||||||
async 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`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def convert(self, ctx, argument):
|
|
||||||
try:
|
|
||||||
invite = await ctx.bot.get_invite(argument)
|
|
||||||
return invite
|
|
||||||
except Exception as exc:
|
|
||||||
raise BadArgument("Invite is invalid or expired") from exc
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def convert(self, ctx, argument):
|
|
||||||
match = self._get_id_match(argument) or re.match(
|
|
||||||
r"<a?:[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 PartialEmojiConverter(Converter):
|
|
||||||
"""Converts to a :class:`PartialEmoji`.
|
|
||||||
|
|
||||||
|
|
||||||
This is done by extracting the animated flag, name and ID from the emoji.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def convert(self, ctx, argument):
|
|
||||||
match = re.match(r"<(a?):([a-zA-Z0-9\_]+):([0-9]+)>$", argument)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
emoji_animated = bool(match.group(1))
|
|
||||||
emoji_name = match.group(2)
|
|
||||||
emoji_id = int(match.group(3))
|
|
||||||
|
|
||||||
return discord.PartialEmoji(animated=emoji_animated, name=emoji_name, id=emoji_id)
|
|
||||||
|
|
||||||
raise BadArgument('Couldn\'t convert "{}" to PartialEmoji.'.format(argument))
|
|
||||||
|
|
||||||
|
|
||||||
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: :obj:`bool`
|
|
||||||
Whether to clean channel mentions.
|
|
||||||
use_nicknames: :obj:`bool`
|
|
||||||
Whether to use nicknames when transforming mentions.
|
|
||||||
escape_markdown: :obj:`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
|
|
||||||
|
|
||||||
async 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=ctx.guild.get_role):
|
|
||||||
r = _find(_id)
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class _Greedy:
|
|
||||||
__slots__ = ("converter",)
|
|
||||||
|
|
||||||
def __init__(self, *, converter=None):
|
|
||||||
self.converter = converter
|
|
||||||
|
|
||||||
def __getitem__(self, params):
|
|
||||||
if not isinstance(params, tuple):
|
|
||||||
params = (params,)
|
|
||||||
if len(params) != 1:
|
|
||||||
raise TypeError("Greedy[...] only takes a single argument")
|
|
||||||
converter = params[0]
|
|
||||||
|
|
||||||
if not inspect.isclass(converter):
|
|
||||||
raise TypeError("Greedy[...] expects a type.")
|
|
||||||
|
|
||||||
if converter is str or converter is type(None) or converter is _Greedy:
|
|
||||||
raise TypeError("Greedy[%s] is invalid." % converter.__name__)
|
|
||||||
|
|
||||||
return self.__class__(converter=converter)
|
|
||||||
|
|
||||||
|
|
||||||
Greedy = _Greedy()
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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
|
|
||||||
member = 4
|
|
||||||
category = 5
|
|
||||||
|
|
||||||
|
|
||||||
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 get_tokens(self, current=None):
|
|
||||||
if not current:
|
|
||||||
current = time.time()
|
|
||||||
|
|
||||||
tokens = self._tokens
|
|
||||||
|
|
||||||
if current > self._window + self.per:
|
|
||||||
tokens = self.rate
|
|
||||||
return tokens
|
|
||||||
|
|
||||||
def update_rate_limit(self):
|
|
||||||
current = time.time()
|
|
||||||
self._last = current
|
|
||||||
|
|
||||||
self._tokens = self.get_tokens(current)
|
|
||||||
|
|
||||||
# first token used means that we start a new rate limit window
|
|
||||||
if self._tokens == self.rate:
|
|
||||||
self._window = current
|
|
||||||
|
|
||||||
# check if we are 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, msg):
|
|
||||||
bucket_type = self._cooldown.type
|
|
||||||
if bucket_type is BucketType.user:
|
|
||||||
return msg.author.id
|
|
||||||
elif bucket_type is BucketType.guild:
|
|
||||||
return (msg.guild or msg.author).id
|
|
||||||
elif bucket_type is BucketType.channel:
|
|
||||||
return msg.channel.id
|
|
||||||
elif bucket_type is BucketType.member:
|
|
||||||
return ((msg.guild and msg.guild.id), msg.author.id)
|
|
||||||
elif bucket_type is BucketType.category:
|
|
||||||
return (msg.channel.category or 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, message):
|
|
||||||
if self._cooldown.type is BucketType.default:
|
|
||||||
return self._cooldown
|
|
||||||
|
|
||||||
self._verify_cache_integrity()
|
|
||||||
key = self._bucket_key(message)
|
|
||||||
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
@@ -1,279 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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",
|
|
||||||
"ConversionError",
|
|
||||||
"BadUnionArgument",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class CommandError(DiscordException):
|
|
||||||
r"""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 ConversionError(CommandError):
|
|
||||||
"""Exception raised when a Converter class raises non-CommandError.
|
|
||||||
|
|
||||||
This inherits from :exc:`.CommandError`.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
converter: :class:`discord.ext.commands.Converter`
|
|
||||||
The converter that failed.
|
|
||||||
original
|
|
||||||
The original exception that was raised. You can also get this via
|
|
||||||
the ``__cause__`` attribute.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, converter, original):
|
|
||||||
self.converter = converter
|
|
||||||
self.original = original
|
|
||||||
|
|
||||||
|
|
||||||
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: :class:`inspect.Parameter`
|
|
||||||
The argument that is missing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, param):
|
|
||||||
self.param = param
|
|
||||||
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: :class:`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: :class:`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: :class:`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)
|
|
||||||
|
|
||||||
|
|
||||||
class BadUnionArgument(UserInputError):
|
|
||||||
"""Exception raised when a :class:`typing.Union` converter fails for all
|
|
||||||
its associated types.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
-----------
|
|
||||||
param: :class:`inspect.Parameter`
|
|
||||||
The parameter that failed being converted.
|
|
||||||
converters: Tuple[Type, ...]
|
|
||||||
A tuple of converters attempted in conversion, in order of failure.
|
|
||||||
errors: List[:class:`CommandError`]
|
|
||||||
A list of errors that were caught from failing the conversion.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, param, converters, errors):
|
|
||||||
self.param = param
|
|
||||||
self.converters = converters
|
|
||||||
self.errors = errors
|
|
||||||
|
|
||||||
def _get_name(x):
|
|
||||||
try:
|
|
||||||
return x.__name__
|
|
||||||
except AttributeError:
|
|
||||||
return x.__class__.__name__
|
|
||||||
|
|
||||||
to_string = [_get_name(x) for x in converters]
|
|
||||||
if len(to_string) > 2:
|
|
||||||
fmt = "{}, or {}".format(", ".join(to_string[:-1]), to_string[-1])
|
|
||||||
else:
|
|
||||||
fmt = " or ".join(to_string)
|
|
||||||
|
|
||||||
super().__init__('Could not convert "{0.name}" into {1}.'.format(param, fmt))
|
|
||||||
@@ -1,370 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 discord.utils
|
|
||||||
|
|
||||||
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: :class:`str`
|
|
||||||
The prefix inserted to every page. e.g. three backticks.
|
|
||||||
suffix: :class:`str`
|
|
||||||
The suffix appended at the end of every page. e.g. three backticks.
|
|
||||||
max_size: :class:`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: :class:`bool`
|
|
||||||
Dictates if hidden commands should be shown in the output.
|
|
||||||
Defaults to ``False``.
|
|
||||||
show_check_failure: :class:`bool`
|
|
||||||
Dictates if commands that have their :attr:`.Command.checks` failed
|
|
||||||
shown. Defaults to ``False``.
|
|
||||||
width: :class:`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):
|
|
||||||
""":class:`bool`: Specifies if the command has subcommands."""
|
|
||||||
return isinstance(self.command, GroupMixin)
|
|
||||||
|
|
||||||
def is_bot(self):
|
|
||||||
""":class:`bool`: Specifies if the command being formatted is the bot itself."""
|
|
||||||
return self.command is self.context.bot
|
|
||||||
|
|
||||||
def is_cog(self):
|
|
||||||
""":class:`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):
|
|
||||||
""":class:`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: discord.utils._string_width(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.guild.me if self.context.guild else 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.display_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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async 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) :class:`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
|
|
||||||
|
|
||||||
async def predicate(tup):
|
|
||||||
if sane_no_suspension_point_predicate(tup) is False:
|
|
||||||
return False
|
|
||||||
|
|
||||||
cmd = tup[1]
|
|
||||||
try:
|
|
||||||
return await 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 = await 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
|
|
||||||
width_gap = discord.utils._string_width(name) - len(name)
|
|
||||||
entry = " {0:<{width}} {1}".format(
|
|
||||||
name, command.short_doc, width=max_width - width_gap
|
|
||||||
)
|
|
||||||
shortened = self.shorten(entry)
|
|
||||||
self._paginator.add_line(shortened)
|
|
||||||
|
|
||||||
async 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 await self.format()
|
|
||||||
|
|
||||||
async 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 = await 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
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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
|
|
||||||
|
|
||||||
# map from opening quotes to closing quotes
|
|
||||||
_quotes = {
|
|
||||||
'"': '"',
|
|
||||||
"‘": "’",
|
|
||||||
"‚": "‛",
|
|
||||||
"“": "”",
|
|
||||||
"„": "‟",
|
|
||||||
"⹂": "⹂",
|
|
||||||
"「": "」",
|
|
||||||
"『": "』",
|
|
||||||
"〝": "〞",
|
|
||||||
"﹁": "﹂",
|
|
||||||
"﹃": "﹄",
|
|
||||||
""": """,
|
|
||||||
"「": "」",
|
|
||||||
"«": "»",
|
|
||||||
"‹": "›",
|
|
||||||
"《": "》",
|
|
||||||
"〈": "〉",
|
|
||||||
}
|
|
||||||
_all_quotes = set(_quotes.keys()) | set(_quotes.values())
|
|
||||||
|
|
||||||
|
|
||||||
def quoted_word(view):
|
|
||||||
current = view.current
|
|
||||||
|
|
||||||
if current is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
close_quote = _quotes.get(current)
|
|
||||||
is_quoted = bool(close_quote)
|
|
||||||
if is_quoted:
|
|
||||||
result = []
|
|
||||||
_escaped_quotes = (current, close_quote)
|
|
||||||
else:
|
|
||||||
result = [current]
|
|
||||||
_escaped_quotes = _all_quotes
|
|
||||||
|
|
||||||
while not view.eof:
|
|
||||||
current = view.get()
|
|
||||||
if not current:
|
|
||||||
if is_quoted:
|
|
||||||
# unexpected EOF
|
|
||||||
raise BadArgument("Expected closing {}.".format(close_quote))
|
|
||||||
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 {}.".format(close_quote))
|
|
||||||
# if we aren't then we just let it through
|
|
||||||
return "".join(result)
|
|
||||||
|
|
||||||
if next_char in _escaped_quotes:
|
|
||||||
# escaped quote
|
|
||||||
result.append(next_char)
|
|
||||||
else:
|
|
||||||
# different escape character, ignore it
|
|
||||||
view.undo()
|
|
||||||
result.append(current)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not is_quoted and current in _all_quotes:
|
|
||||||
# we aren't quoted
|
|
||||||
raise BadArgument("Unexpected quote mark in non-quoted string")
|
|
||||||
|
|
||||||
# closing quote
|
|
||||||
if is_quoted and current == close_quote:
|
|
||||||
next_char = view.get()
|
|
||||||
valid_eof = not next_char or next_char.isspace()
|
|
||||||
if not valid_eof:
|
|
||||||
raise BadArgument("Expected space after closing quotation")
|
|
||||||
|
|
||||||
# we're quoted so it's okay
|
|
||||||
return "".join(result)
|
|
||||||
|
|
||||||
if current.isspace() and not is_quoted:
|
|
||||||
# end of word found
|
|
||||||
return "".join(result)
|
|
||||||
|
|
||||||
result.append(current)
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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[:class:`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[:class:`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.
|
|
||||||
spoiler: :class:`bool`
|
|
||||||
Whether the attachment is a spoiler.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("fp", "filename", "_true_fp")
|
|
||||||
|
|
||||||
def __init__(self, fp, filename=None, *, spoiler=False):
|
|
||||||
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
|
|
||||||
|
|
||||||
if spoiler and not self.filename.startswith("SPOILER_"):
|
|
||||||
self.filename = "SPOILER_" + self.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()
|
|
||||||
@@ -1,735 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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
|
|
||||||
import concurrent.futures
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import struct
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import zlib
|
|
||||||
|
|
||||||
import websockets
|
|
||||||
|
|
||||||
from . import utils
|
|
||||||
from .activity import _ActivityTag
|
|
||||||
from .enums import SpeakingState
|
|
||||||
from .errors import ConnectionClosed, InvalidArgument
|
|
||||||
|
|
||||||
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.block_msg = "Heartbeat blocked for more than %s seconds."
|
|
||||||
self.behind_msg = "Can't keep up, websocket is %.1fs behind."
|
|
||||||
self._stop_ev = threading.Event()
|
|
||||||
self._last_ack = time.perf_counter()
|
|
||||||
self._last_send = time.perf_counter()
|
|
||||||
self.latency = float("inf")
|
|
||||||
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.perf_counter():
|
|
||||||
log.warning(
|
|
||||||
"Shard ID %s has stopped responding to the gateway. Closing and restarting.",
|
|
||||||
self.shard_id,
|
|
||||||
)
|
|
||||||
coro = self.ws.close(4000)
|
|
||||||
f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop)
|
|
||||||
|
|
||||||
try:
|
|
||||||
f.result()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
self.stop()
|
|
||||||
return
|
|
||||||
|
|
||||||
data = self.get_payload()
|
|
||||||
log.debug(self.msg, data["d"])
|
|
||||||
coro = self.ws.send_as_json(data)
|
|
||||||
f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop)
|
|
||||||
try:
|
|
||||||
# block until sending is complete
|
|
||||||
total = 0
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
f.result(5)
|
|
||||||
break
|
|
||||||
except concurrent.futures.TimeoutError:
|
|
||||||
total += 5
|
|
||||||
log.warning(self.block_msg, total)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
self.stop()
|
|
||||||
else:
|
|
||||||
self._last_send = time.perf_counter()
|
|
||||||
|
|
||||||
def get_payload(self):
|
|
||||||
return {"op": self.ws.HEARTBEAT, "d": self.ws.sequence}
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self._stop_ev.set()
|
|
||||||
|
|
||||||
def ack(self):
|
|
||||||
ack_time = time.perf_counter()
|
|
||||||
self._last_ack = ack_time
|
|
||||||
self.latency = ack_time - self._last_send
|
|
||||||
if self.latency > 10:
|
|
||||||
log.warning(self.behind_msg, self.latency)
|
|
||||||
|
|
||||||
|
|
||||||
class VoiceKeepAliveHandler(KeepAliveHandler):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.msg = "Keeping voice websocket alive with timestamp %s."
|
|
||||||
self.block_msg = "Voice heartbeat blocked for more than %s seconds"
|
|
||||||
self.behind_msg = "Can't keep up, voice websocket is %.1fs behind"
|
|
||||||
|
|
||||||
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
|
|
||||||
self._zlib = zlib.decompressobj()
|
|
||||||
self._buffer = bytearray()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async 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 = await client.http.get_gateway()
|
|
||||||
ws = await websockets.connect(gateway, loop=client.loop, klass=cls, compression=None)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
await ws.poll_event()
|
|
||||||
|
|
||||||
if not resume:
|
|
||||||
await ws.identify()
|
|
||||||
return ws
|
|
||||||
|
|
||||||
await ws.resume()
|
|
||||||
try:
|
|
||||||
await 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 await 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 = self.loop.create_future()
|
|
||||||
entry = EventListener(event=event, predicate=predicate, result=result, future=future)
|
|
||||||
self._dispatch_listeners.append(entry)
|
|
||||||
return future
|
|
||||||
|
|
||||||
async 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._activity is not None or state._status is not None:
|
|
||||||
payload["d"]["presence"] = {
|
|
||||||
"status": state._status,
|
|
||||||
"game": state._activity,
|
|
||||||
"since": 0,
|
|
||||||
"afk": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.send_as_json(payload)
|
|
||||||
log.info("Shard ID %s has sent the IDENTIFY payload.", self.shard_id)
|
|
||||||
|
|
||||||
async def resume(self):
|
|
||||||
"""Sends the RESUME packet."""
|
|
||||||
payload = {
|
|
||||||
"op": self.RESUME,
|
|
||||||
"d": {"seq": self.sequence, "session_id": self.session_id, "token": self.token},
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.send_as_json(payload)
|
|
||||||
log.info("Shard ID %s has sent the RESUME payload.", self.shard_id)
|
|
||||||
|
|
||||||
async def received_message(self, msg):
|
|
||||||
self._dispatch("socket_raw_receive", msg)
|
|
||||||
|
|
||||||
if type(msg) is bytes:
|
|
||||||
self._buffer.extend(msg)
|
|
||||||
|
|
||||||
if len(msg) >= 4:
|
|
||||||
if msg[-4:] == b"\x00\x00\xff\xff":
|
|
||||||
msg = self._zlib.decompress(self._buffer)
|
|
||||||
msg = msg.decode("utf-8")
|
|
||||||
self._buffer = bytearray()
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
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.DISPATCH:
|
|
||||||
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.")
|
|
||||||
await 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()
|
|
||||||
await 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
|
|
||||||
await self.send_as_json(self._keep_alive.get_payload())
|
|
||||||
self._keep_alive.start()
|
|
||||||
return
|
|
||||||
|
|
||||||
if op == self.INVALIDATE_SESSION:
|
|
||||||
if data is True:
|
|
||||||
await asyncio.sleep(5.0, loop=self.loop)
|
|
||||||
await 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)
|
|
||||||
await self.identify()
|
|
||||||
return
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
elif 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 exc:
|
|
||||||
future.set_exception(exc)
|
|
||||||
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):
|
|
||||||
""":obj:`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.latency
|
|
||||||
|
|
||||||
def _can_handle_close(self, code):
|
|
||||||
return code not in (1000, 4004, 4010, 4011)
|
|
||||||
|
|
||||||
async 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 = await self.recv()
|
|
||||||
await self.received_message(msg)
|
|
||||||
except websockets.exceptions.ConnectionClosed as exc:
|
|
||||||
if self._can_handle_close(exc.code):
|
|
||||||
log.info(
|
|
||||||
"Websocket closed with %s (%s), attempting a reconnect.", exc.code, exc.reason
|
|
||||||
)
|
|
||||||
raise ResumeWebSocket(self.shard_id) from exc
|
|
||||||
else:
|
|
||||||
log.info("Websocket closed with %s (%s), cannot reconnect.", exc.code, exc.reason)
|
|
||||||
raise ConnectionClosed(exc, shard_id=self.shard_id) from exc
|
|
||||||
|
|
||||||
async def send(self, data):
|
|
||||||
self._dispatch("socket_raw_send", data)
|
|
||||||
await super().send(data)
|
|
||||||
|
|
||||||
async def send_as_json(self, data):
|
|
||||||
try:
|
|
||||||
await self.send(utils.to_json(data))
|
|
||||||
except websockets.exceptions.ConnectionClosed as exc:
|
|
||||||
if not self._can_handle_close(exc.code):
|
|
||||||
raise ConnectionClosed(exc, shard_id=self.shard_id) from exc
|
|
||||||
|
|
||||||
async def change_presence(self, *, activity=None, status=None, afk=False, since=0.0):
|
|
||||||
if activity is not None:
|
|
||||||
if not isinstance(activity, _ActivityTag):
|
|
||||||
raise InvalidArgument("activity must be one of Game, Streaming, or Activity.")
|
|
||||||
activity = activity.to_dict()
|
|
||||||
|
|
||||||
if status == "idle":
|
|
||||||
since = int(time.time() * 1000)
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"op": self.PRESENCE,
|
|
||||||
"d": {"game": activity, "afk": afk, "since": since, "status": status},
|
|
||||||
}
|
|
||||||
|
|
||||||
sent = utils.to_json(payload)
|
|
||||||
log.debug('Sending "%s" to change status', sent)
|
|
||||||
await self.send(sent)
|
|
||||||
|
|
||||||
async def request_sync(self, guild_ids):
|
|
||||||
payload = {"op": self.GUILD_SYNC, "d": list(guild_ids)}
|
|
||||||
await self.send_as_json(payload)
|
|
||||||
|
|
||||||
async 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)
|
|
||||||
await self.send_as_json(payload)
|
|
||||||
|
|
||||||
async def close(self, code=1000, reason=""):
|
|
||||||
if self._keep_alive:
|
|
||||||
self._keep_alive.stop()
|
|
||||||
|
|
||||||
await super().close(code, reason)
|
|
||||||
|
|
||||||
async def close_connection(self, *args, **kwargs):
|
|
||||||
if self._keep_alive:
|
|
||||||
self._keep_alive.stop()
|
|
||||||
|
|
||||||
await super().close_connection(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
|
||||||
CLIENT_CONNECT
|
|
||||||
Indicates a user has connected to voice.
|
|
||||||
CLIENT_DISCONNECT
|
|
||||||
Receive only. Indicates a user has disconnected from voice.
|
|
||||||
"""
|
|
||||||
|
|
||||||
IDENTIFY = 0
|
|
||||||
SELECT_PROTOCOL = 1
|
|
||||||
READY = 2
|
|
||||||
HEARTBEAT = 3
|
|
||||||
SESSION_DESCRIPTION = 4
|
|
||||||
SPEAKING = 5
|
|
||||||
HEARTBEAT_ACK = 6
|
|
||||||
RESUME = 7
|
|
||||||
HELLO = 8
|
|
||||||
INVALIDATE_SESSION = 9
|
|
||||||
CLIENT_CONNECT = 12
|
|
||||||
CLIENT_DISCONNECT = 13
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.max_size = None
|
|
||||||
self._keep_alive = None
|
|
||||||
|
|
||||||
async def send_as_json(self, data):
|
|
||||||
log.debug("Sending voice websocket frame: %s.", data)
|
|
||||||
await self.send(utils.to_json(data))
|
|
||||||
|
|
||||||
async 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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await self.send_as_json(payload)
|
|
||||||
|
|
||||||
async 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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await self.send_as_json(payload)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def from_client(cls, client, *, resume=False):
|
|
||||||
"""Creates a voice websocket for the :class:`VoiceClient`."""
|
|
||||||
gateway = "wss://" + client.endpoint + "/?v=4"
|
|
||||||
ws = await websockets.connect(gateway, loop=client.loop, klass=cls, compression=None)
|
|
||||||
ws.gateway = gateway
|
|
||||||
ws._connection = client
|
|
||||||
ws._max_heartbeat_timeout = 60.0
|
|
||||||
|
|
||||||
if resume:
|
|
||||||
await ws.resume()
|
|
||||||
else:
|
|
||||||
await ws.identify()
|
|
||||||
|
|
||||||
return ws
|
|
||||||
|
|
||||||
async def select_protocol(self, ip, port, mode):
|
|
||||||
payload = {
|
|
||||||
"op": self.SELECT_PROTOCOL,
|
|
||||||
"d": {"protocol": "udp", "data": {"address": ip, "port": port, "mode": mode}},
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.send_as_json(payload)
|
|
||||||
|
|
||||||
async def client_connect(self):
|
|
||||||
payload = {"op": self.CLIENT_CONNECT, "d": {"audio_ssrc": self._connection.ssrc}}
|
|
||||||
|
|
||||||
await self.send_as_json(payload)
|
|
||||||
|
|
||||||
async def speak(self, state=SpeakingState.voice):
|
|
||||||
payload = {"op": self.SPEAKING, "d": {"speaking": int(state), "delay": 0}}
|
|
||||||
|
|
||||||
await self.send_as_json(payload)
|
|
||||||
|
|
||||||
async def received_message(self, msg):
|
|
||||||
log.debug("Voice websocket frame received: %s", msg)
|
|
||||||
op = msg["op"]
|
|
||||||
data = msg.get("d")
|
|
||||||
|
|
||||||
if op == self.READY:
|
|
||||||
await self.initial_connection(data)
|
|
||||||
elif op == self.HEARTBEAT_ACK:
|
|
||||||
self._keep_alive.ack()
|
|
||||||
elif op == self.INVALIDATE_SESSION:
|
|
||||||
log.info("Voice RESUME failed.")
|
|
||||||
await self.identify()
|
|
||||||
elif op == self.SESSION_DESCRIPTION:
|
|
||||||
self._connection.mode = data["mode"]
|
|
||||||
await self.load_secret_key(data)
|
|
||||||
elif op == self.HELLO:
|
|
||||||
interval = data["heartbeat_interval"] / 1000.0
|
|
||||||
self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=interval)
|
|
||||||
self._keep_alive.start()
|
|
||||||
|
|
||||||
async 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 = await 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)
|
|
||||||
|
|
||||||
# there *should* always be at least one supported mode (xsalsa20_poly1305)
|
|
||||||
modes = [mode for mode in data["modes"] if mode in self._connection.supported_modes]
|
|
||||||
log.debug("received supported encryption modes: %s", ", ".join(modes))
|
|
||||||
|
|
||||||
mode = modes[0]
|
|
||||||
await self.select_protocol(state.ip, state.port, mode)
|
|
||||||
log.info("selected the voice protocol for use (%s)", mode)
|
|
||||||
|
|
||||||
await self.client_connect()
|
|
||||||
|
|
||||||
async def load_secret_key(self, data):
|
|
||||||
log.info("received secret key for voice connection")
|
|
||||||
self._connection.secret_key = data.get("secret_key")
|
|
||||||
await self.speak()
|
|
||||||
await self.speak(False)
|
|
||||||
|
|
||||||
async def poll_event(self):
|
|
||||||
try:
|
|
||||||
msg = await asyncio.wait_for(self.recv(), timeout=30.0, loop=self.loop)
|
|
||||||
await self.received_message(json.loads(msg))
|
|
||||||
except websockets.exceptions.ConnectionClosed as exc:
|
|
||||||
raise ConnectionClosed(exc, shard_id=None) from exc
|
|
||||||
|
|
||||||
async def close_connection(self, *args, **kwargs):
|
|
||||||
if self._keep_alive:
|
|
||||||
self._keep_alive.stop()
|
|
||||||
|
|
||||||
await super().close_connection(*args, **kwargs)
|
|
||||||
1452
discord/guild.py
1452
discord/guild.py
File diff suppressed because it is too large
Load Diff
909
discord/http.py
909
discord/http.py
@@ -1,909 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 json
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
from urllib.parse import quote as _uriquote
|
|
||||||
import weakref
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
from .errors import HTTPException, Forbidden, NotFound, LoginFailure, GatewayNotFound
|
|
||||||
from . import __version__, utils
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def json_or_text(response):
|
|
||||||
text = await 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(
|
|
||||||
**{k: _uriquote(v) if isinstance(v, str) else v for k, v in parameters.items()}
|
|
||||||
)
|
|
||||||
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__)
|
|
||||||
|
|
||||||
def recreate(self):
|
|
||||||
if self._session.closed:
|
|
||||||
self._session = aiohttp.ClientSession(connector=self.connector, loop=self.loop)
|
|
||||||
|
|
||||||
async 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
|
|
||||||
await self._global_over.wait()
|
|
||||||
|
|
||||||
await lock.acquire()
|
|
||||||
with MaybeUnlock(lock) as maybe_lock:
|
|
||||||
for tries in range(5):
|
|
||||||
async with self._session.request(method, url, **kwargs) as r:
|
|
||||||
log.debug(
|
|
||||||
"%s %s with %s has returned %s", method, url, kwargs.get("data"), r.status
|
|
||||||
)
|
|
||||||
|
|
||||||
# even errors have text involved in them so this is safe to call
|
|
||||||
data = await 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.debug(
|
|
||||||
"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.warning(fmt, retry_after, bucket)
|
|
||||||
|
|
||||||
# check if it's a global rate limit
|
|
||||||
is_global = data.get("global", False)
|
|
||||||
if is_global:
|
|
||||||
log.warning(
|
|
||||||
"Global rate limit has been hit. Retrying in %.2f seconds.",
|
|
||||||
retry_after,
|
|
||||||
)
|
|
||||||
self._global_over.clear()
|
|
||||||
|
|
||||||
await 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}:
|
|
||||||
await 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)
|
|
||||||
|
|
||||||
# We've run out of retries, raise.
|
|
||||||
raise HTTPException(r, data)
|
|
||||||
|
|
||||||
async def get_attachment(self, url):
|
|
||||||
async with self._session.get(url) as resp:
|
|
||||||
if resp.status == 200:
|
|
||||||
return await 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")
|
|
||||||
|
|
||||||
# state management
|
|
||||||
|
|
||||||
async def close(self):
|
|
||||||
await self._session.close()
|
|
||||||
|
|
||||||
def _token(self, token, *, bot=True):
|
|
||||||
self.token = token
|
|
||||||
self.bot_token = bot
|
|
||||||
self._ack_token = None
|
|
||||||
|
|
||||||
# login management
|
|
||||||
|
|
||||||
async def static_login(self, token, *, bot):
|
|
||||||
old_token, old_bot = self.token, self.bot_token
|
|
||||||
self._token(token, bot=bot)
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = await self.request(Route("GET", "/users/@me"))
|
|
||||||
except HTTPException as exc:
|
|
||||||
self._token(old_token, bot=old_bot)
|
|
||||||
if exc.response.status == 401:
|
|
||||||
raise LoginFailure("Improper token has been passed.") from exc
|
|
||||||
raise
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
async 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 = await 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 remove_own_reaction(self, message_id, channel_id, emoji):
|
|
||||||
r = Route(
|
|
||||||
"DELETE",
|
|
||||||
"/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 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",
|
|
||||||
"parent_id",
|
|
||||||
"topic",
|
|
||||||
"bitrate",
|
|
||||||
"nsfw",
|
|
||||||
"user_limit",
|
|
||||||
"position",
|
|
||||||
"permission_overwrites",
|
|
||||||
"rate_limit_per_user",
|
|
||||||
)
|
|
||||||
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, channel_type, *, reason=None, **options):
|
|
||||||
payload = {"type": channel_type}
|
|
||||||
|
|
||||||
valid_keys = (
|
|
||||||
"name",
|
|
||||||
"parent_id",
|
|
||||||
"topic",
|
|
||||||
"bitrate",
|
|
||||||
"nsfw",
|
|
||||||
"user_limit",
|
|
||||||
"position",
|
|
||||||
"permission_overwrites",
|
|
||||||
"rate_limit_per_user",
|
|
||||||
)
|
|
||||||
payload.update({k: v for k, v in options.items() if k in valid_keys and v is not None})
|
|
||||||
|
|
||||||
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, avatar=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",
|
|
||||||
"default_message_notifications",
|
|
||||||
"explicit_content_filter",
|
|
||||||
)
|
|
||||||
|
|
||||||
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_ban(self, user_id, guild_id):
|
|
||||||
return self.request(
|
|
||||||
Route("GET", "/guilds/{guild_id}/bans/{user_id}", guild_id=guild_id, user_id=user_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, *, roles=None, reason=None):
|
|
||||||
payload = {"name": name, "image": image, "roles": roles or []}
|
|
||||||
|
|
||||||
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, roles=None, reason=None):
|
|
||||||
payload = {"name": name, "roles": roles or []}
|
|
||||||
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"))
|
|
||||||
|
|
||||||
async def get_gateway(self, *, encoding="json", v=6, zlib=True):
|
|
||||||
try:
|
|
||||||
data = await self.request(Route("GET", "/gateway"))
|
|
||||||
except HTTPException as exc:
|
|
||||||
raise GatewayNotFound() from exc
|
|
||||||
if zlib:
|
|
||||||
value = "{0}?encoding={1}&v={2}&compress=zlib-stream"
|
|
||||||
else:
|
|
||||||
value = "{0}?encoding={1}&v={2}"
|
|
||||||
return value.format(data["url"], encoding, v)
|
|
||||||
|
|
||||||
async def get_bot_gateway(self, *, encoding="json", v=6, zlib=True):
|
|
||||||
try:
|
|
||||||
data = await self.request(Route("GET", "/gateway/bot"))
|
|
||||||
except HTTPException as exc:
|
|
||||||
raise GatewayNotFound() from exc
|
|
||||||
|
|
||||||
if zlib:
|
|
||||||
value = "{0}?encoding={1}&v={2}&compress=zlib-stream"
|
|
||||||
else:
|
|
||||||
value = "{0}?encoding={1}&v={2}"
|
|
||||||
return data["shards"], value.format(data["url"], encoding, v)
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
def get_mutual_friends(self, user_id):
|
|
||||||
return self.request(Route("GET", "/users/{user_id}/relationships", user_id=user_id))
|
|
||||||
|
|
||||||
def change_hypesquad_house(self, house_id):
|
|
||||||
payload = {"house_id": house_id}
|
|
||||||
return self.request(Route("POST", "/hypesquad/online"), json=payload)
|
|
||||||
|
|
||||||
def leave_hypesquad_house(self):
|
|
||||||
return self.request(Route("DELETE", "/hypesquad/online"))
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 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: :class:`int`
|
|
||||||
How long the before the invite expires in seconds. A value of 0 indicates that it doesn't expire.
|
|
||||||
code: :class:`str`
|
|
||||||
The URL fragment used for the invite.
|
|
||||||
guild: :class:`Guild`
|
|
||||||
The guild the invite is for.
|
|
||||||
revoked: :class:`bool`
|
|
||||||
Indicates if the invite has been revoked.
|
|
||||||
created_at: `datetime.datetime`
|
|
||||||
A datetime object denoting the time the invite was created.
|
|
||||||
temporary: :class:`bool`
|
|
||||||
Indicates that the invite grants temporary membership.
|
|
||||||
If True, members who joined via this invite will be kicked upon disconnect.
|
|
||||||
uses: :class:`int`
|
|
||||||
How many times the invite has been used.
|
|
||||||
max_uses: :class:`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"]
|
|
||||||
|
|
||||||
guild.splash = data["guild"]["splash"]
|
|
||||||
guild.splash_url = ""
|
|
||||||
if guild.splash:
|
|
||||||
guild.splash_url = "https://cdn.discordapp.com/splashes/{0.id}/{0.splash}.jpg?size=2048".format(
|
|
||||||
guild
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
async def delete(self, *, reason=None):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Revokes the instant invite.
|
|
||||||
|
|
||||||
You must have the :attr:`~Permissions.manage_channels` permission to do this.
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
await self._state.http.delete_invite(self.code, reason=reason)
|
|
||||||
@@ -1,489 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 datetime
|
|
||||||
|
|
||||||
from .errors import NoMoreItems
|
|
||||||
from .utils import time_snowflake, maybe_coroutine
|
|
||||||
from .object import Object
|
|
||||||
from .audit_logs import AuditLogEntry
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
async def find(self, predicate):
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
elem = await self.next()
|
|
||||||
except NoMoreItems:
|
|
||||||
return None
|
|
||||||
|
|
||||||
ret = await maybe_coroutine(predicate, elem)
|
|
||||||
if ret:
|
|
||||||
return elem
|
|
||||||
|
|
||||||
def map(self, func):
|
|
||||||
return _MappedAsyncIterator(self, func)
|
|
||||||
|
|
||||||
def filter(self, predicate):
|
|
||||||
return _FilteredAsyncIterator(self, predicate)
|
|
||||||
|
|
||||||
async def flatten(self):
|
|
||||||
ret = []
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
item = await self.next()
|
|
||||||
except NoMoreItems:
|
|
||||||
return ret
|
|
||||||
else:
|
|
||||||
ret.append(item)
|
|
||||||
|
|
||||||
def __aiter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __anext__(self):
|
|
||||||
try:
|
|
||||||
msg = await 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
|
|
||||||
|
|
||||||
async def next(self):
|
|
||||||
# this raises NoMoreItems and will propagate appropriately
|
|
||||||
item = await self.iterator.next()
|
|
||||||
return await 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
|
|
||||||
|
|
||||||
async def next(self):
|
|
||||||
getter = self.iterator.next
|
|
||||||
pred = self.predicate
|
|
||||||
while True:
|
|
||||||
# propagate NoMoreItems similar to _MappedAsyncIterator
|
|
||||||
item = await getter()
|
|
||||||
ret = await 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)
|
|
||||||
|
|
||||||
async def next(self):
|
|
||||||
if self.users.empty():
|
|
||||||
await self.fill_users()
|
|
||||||
|
|
||||||
try:
|
|
||||||
return self.users.get_nowait()
|
|
||||||
except asyncio.QueueEmpty:
|
|
||||||
raise NoMoreItems()
|
|
||||||
|
|
||||||
async 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 = await self.getter(
|
|
||||||
self.message.id, self.channel_id, self.emoji, retrieve, after=after
|
|
||||||
)
|
|
||||||
|
|
||||||
if data:
|
|
||||||
self.limit -= retrieve
|
|
||||||
self.after = Object(id=int(data[-1]["id"]))
|
|
||||||
|
|
||||||
if self.guild is None:
|
|
||||||
for element in reversed(data):
|
|
||||||
await 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:
|
|
||||||
await self.users.put(member)
|
|
||||||
else:
|
|
||||||
await 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
|
|
||||||
|
|
||||||
async def next(self):
|
|
||||||
if self.messages.empty():
|
|
||||||
await 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
|
|
||||||
|
|
||||||
async 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 = await self.messageable._get_channel()
|
|
||||||
self.channel = channel
|
|
||||||
while self._get_retrieve():
|
|
||||||
data = await 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
|
|
||||||
|
|
||||||
async def fill_messages(self):
|
|
||||||
if not hasattr(self, "channel"):
|
|
||||||
# do the required set up
|
|
||||||
channel = await self.messageable._get_channel()
|
|
||||||
self.channel = channel
|
|
||||||
|
|
||||||
if self._get_retrieve():
|
|
||||||
data = await 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:
|
|
||||||
await self.messages.put(self.state.create_message(channel=channel, data=element))
|
|
||||||
|
|
||||||
async def _retrieve_messages(self, retrieve):
|
|
||||||
"""Retrieve messages and update next parameters."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def _retrieve_messages_before_strategy(self, retrieve):
|
|
||||||
"""Retrieve messages using before parameter."""
|
|
||||||
before = self.before.id if self.before else None
|
|
||||||
data = await 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
|
|
||||||
|
|
||||||
async def _retrieve_messages_after_strategy(self, retrieve):
|
|
||||||
"""Retrieve messages using after parameter."""
|
|
||||||
after = self.after.id if self.after else None
|
|
||||||
data = await 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
|
|
||||||
|
|
||||||
async 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 = await 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
|
|
||||||
|
|
||||||
async def _before_strategy(self, retrieve):
|
|
||||||
before = self.before.id if self.before else None
|
|
||||||
data = await 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
|
|
||||||
|
|
||||||
async def _after_strategy(self, retrieve):
|
|
||||||
after = self.after.id if self.after else None
|
|
||||||
data = await 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
|
|
||||||
|
|
||||||
async def next(self):
|
|
||||||
if self.entries.empty():
|
|
||||||
await 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
|
|
||||||
|
|
||||||
async def _fill(self):
|
|
||||||
from .user import User
|
|
||||||
|
|
||||||
if self._get_retrieve():
|
|
||||||
users, data = await 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
|
|
||||||
|
|
||||||
await self.entries.put(
|
|
||||||
AuditLogEntry(data=element, users=self._users, guild=self.guild)
|
|
||||||
)
|
|
||||||
@@ -1,621 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 discord.abc
|
|
||||||
|
|
||||||
from . import utils
|
|
||||||
from .user import BaseUser, User
|
|
||||||
from .activity import create_activity
|
|
||||||
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: :class:`bool`
|
|
||||||
Indicates if the user is currently deafened by the guild.
|
|
||||||
mute: :class:`bool`
|
|
||||||
Indicates if the user is currently muted by the guild.
|
|
||||||
self_mute: :class:`bool`
|
|
||||||
Indicates if the user is currently muted by their own accord.
|
|
||||||
self_deaf: :class:`bool`
|
|
||||||
Indicates if the user is currently deafened by their own accord.
|
|
||||||
afk: :class:`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
|
|
||||||
----------
|
|
||||||
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.
|
|
||||||
activities: Tuple[Union[:class:`Game`, :class:`Streaming`, :class:`Spotify`, :class:`Activity`]]
|
|
||||||
The activities that the user is currently doing.
|
|
||||||
guild: :class:`Guild`
|
|
||||||
The guild that the member belongs to.
|
|
||||||
nick: Optional[:class:`str`]
|
|
||||||
The guild specific nickname of the user.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = (
|
|
||||||
"_roles",
|
|
||||||
"joined_at",
|
|
||||||
"_client_status",
|
|
||||||
"activities",
|
|
||||||
"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._client_status = {None: Status.offline}
|
|
||||||
self.activities = tuple(map(create_activity, data.get("activities", [])))
|
|
||||||
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)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _copy(cls, member):
|
|
||||||
self = cls.__new__(cls) # to bypass __init__
|
|
||||||
|
|
||||||
self._roles = utils.SnowflakeList(member._roles, is_sorted=True)
|
|
||||||
self.joined_at = member.joined_at
|
|
||||||
self._client_status = member._client_status.copy()
|
|
||||||
self.guild = member.guild
|
|
||||||
self.nick = member.nick
|
|
||||||
self.activities = member.activities
|
|
||||||
self._state = member._state
|
|
||||||
self._user = User._copy(member._user)
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def _get_channel(self):
|
|
||||||
ch = await self.create_dm()
|
|
||||||
return ch
|
|
||||||
|
|
||||||
def _update_roles(self, data):
|
|
||||||
self._roles = utils.SnowflakeList(map(int, data["roles"]))
|
|
||||||
|
|
||||||
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.activities = tuple(map(create_activity, data.get("activities", [])))
|
|
||||||
self._client_status = {key: value for key, value in data.get("client_status", {}).items()}
|
|
||||||
self._client_status[None] = data["status"]
|
|
||||||
|
|
||||||
if len(user) > 1:
|
|
||||||
u = self._user
|
|
||||||
u.name = user.get("username", u.name)
|
|
||||||
u.avatar = user.get("avatar", u.avatar)
|
|
||||||
u.discriminator = user.get("discriminator", u.discriminator)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self):
|
|
||||||
""":class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead."""
|
|
||||||
return try_enum(Status, self._client_status[None])
|
|
||||||
|
|
||||||
@status.setter
|
|
||||||
def status(self, value):
|
|
||||||
# internal use only
|
|
||||||
self._client_status[None] = str(value)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mobile_status(self):
|
|
||||||
""":class:`Status`: The member's status on a mobile device, if applicable."""
|
|
||||||
return try_enum(Status, self._client_status.get("mobile", "offline"))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def desktop_status(self):
|
|
||||||
""":class:`Status`: The member's status on the desktop client, if applicable."""
|
|
||||||
return try_enum(Status, self._client_status.get("desktop", "offline"))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def web_status(self):
|
|
||||||
""":class:`Status`: The member's status on the web client, if applicable."""
|
|
||||||
return try_enum(Status, self._client_status.get("web", "offline"))
|
|
||||||
|
|
||||||
def is_on_mobile(self):
|
|
||||||
""":class:`bool`: A helper function that determines if a member is active on a mobile device."""
|
|
||||||
return "mobile" in self._client_status
|
|
||||||
|
|
||||||
@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 roles(self):
|
|
||||||
"""A :class:`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.
|
|
||||||
"""
|
|
||||||
result = []
|
|
||||||
g = self.guild
|
|
||||||
for role_id in self._roles:
|
|
||||||
role = g.get_role(role_id)
|
|
||||||
if role:
|
|
||||||
result.append(role)
|
|
||||||
result.append(g.default_role)
|
|
||||||
result.sort()
|
|
||||||
return result
|
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity(self):
|
|
||||||
"""Returns a class Union[:class:`Game`, :class:`Streaming`, :class:`Spotify`, :class:`Activity`] for the primary
|
|
||||||
activity the user is currently doing. Could be None if no activity is being done.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
A user may have multiple activities, these can be accessed under :attr:`activities`.
|
|
||||||
"""
|
|
||||||
if self.activities:
|
|
||||||
return self.activities[0]
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
async def ban(self, **kwargs):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Bans this member. Equivalent to :meth:`Guild.ban`
|
|
||||||
"""
|
|
||||||
await self.guild.ban(self, **kwargs)
|
|
||||||
|
|
||||||
async def unban(self, *, reason=None):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Unbans this member. Equivalent to :meth:`Guild.unban`
|
|
||||||
"""
|
|
||||||
await self.guild.unban(self, reason=reason)
|
|
||||||
|
|
||||||
async def kick(self, *, reason=None):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Kicks this member. Equivalent to :meth:`Guild.kick`
|
|
||||||
"""
|
|
||||||
await self.guild.kick(self, reason=reason)
|
|
||||||
|
|
||||||
async 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:
|
|
||||||
await 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)
|
|
||||||
|
|
||||||
await http.edit_member(guild_id, self.id, reason=reason, **payload)
|
|
||||||
|
|
||||||
# TODO: wait for WS event for modify-in-place behaviour
|
|
||||||
|
|
||||||
async 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.
|
|
||||||
"""
|
|
||||||
await self.edit(voice_channel=channel, reason=reason)
|
|
||||||
|
|
||||||
async def add_roles(self, *roles, reason=None, atomic=True):
|
|
||||||
r"""|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)
|
|
||||||
await 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:
|
|
||||||
await req(guild_id, user_id, role.id, reason=reason)
|
|
||||||
|
|
||||||
async def remove_roles(self, *roles, reason=None, atomic=True):
|
|
||||||
r"""|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
|
|
||||||
|
|
||||||
await 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:
|
|
||||||
await req(guild_id, user_id, role.id, reason=reason)
|
|
||||||
@@ -1,799 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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
|
|
||||||
from .reaction import Reaction
|
|
||||||
from .emoji import Emoji, PartialEmoji
|
|
||||||
from .calls import CallMessage
|
|
||||||
from .enums import MessageType, try_enum
|
|
||||||
from .errors import InvalidArgument, ClientException, HTTPException
|
|
||||||
from .embeds import Embed
|
|
||||||
|
|
||||||
|
|
||||||
class Attachment:
|
|
||||||
"""Represents an attachment from Discord.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
------------
|
|
||||||
id: :class:`int`
|
|
||||||
The attachment ID.
|
|
||||||
size: :class:`int`
|
|
||||||
The attachment size in bytes.
|
|
||||||
height: Optional[:class:`int`]
|
|
||||||
The attachment's height, in pixels. Only applicable to images.
|
|
||||||
width: Optional[:class:`int`]
|
|
||||||
The attachment's width, in pixels. Only applicable to images.
|
|
||||||
filename: :class:`str`
|
|
||||||
The attachment's filename.
|
|
||||||
url: :class:`str`
|
|
||||||
The attachment URL. If the message this attachment was attached
|
|
||||||
to is deleted, then this will 404.
|
|
||||||
proxy_url: :class:`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
|
|
||||||
|
|
||||||
def is_spoiler(self):
|
|
||||||
""":class:`bool`: Whether this attachment contains a spoiler."""
|
|
||||||
return self.filename.startswith("SPOILER_")
|
|
||||||
|
|
||||||
async def save(self, fp, *, seek_begin=True):
|
|
||||||
"""|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.
|
|
||||||
seek_begin: bool
|
|
||||||
Whether to seek to the beginning of the file after saving is
|
|
||||||
successfully done.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
--------
|
|
||||||
HTTPException
|
|
||||||
Saving the attachment failed.
|
|
||||||
NotFound
|
|
||||||
The attachment was deleted.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
int
|
|
||||||
The number of bytes written.
|
|
||||||
"""
|
|
||||||
|
|
||||||
data = await self._http.get_attachment(self.url)
|
|
||||||
if isinstance(fp, str):
|
|
||||||
with open(fp, "wb") as f:
|
|
||||||
return f.write(data)
|
|
||||||
else:
|
|
||||||
written = fp.write(data)
|
|
||||||
if seek_begin:
|
|
||||||
fp.seek(0)
|
|
||||||
return written
|
|
||||||
|
|
||||||
|
|
||||||
class Message:
|
|
||||||
r"""Represents a message from Discord.
|
|
||||||
|
|
||||||
There should be no need to create one of these manually.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
-----------
|
|
||||||
tts: :class:`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 or the user has the left the guild, then it is a :class:`User` instead.
|
|
||||||
content: :class:`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 of 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: :class:`bool`
|
|
||||||
Specifies if the message mentions everyone.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
This does not check if the ``@everyone`` or the ``@here`` text is in the message itself.
|
|
||||||
Rather this boolean indicates if either the ``@everyone`` or the ``@here`` text is in the message
|
|
||||||
**and** it did end up mentioning.
|
|
||||||
|
|
||||||
mentions: :class:`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: :class:`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: :class:`list`
|
|
||||||
A list of :class:`Role` that were mentioned. If the message is in a private message
|
|
||||||
then the list is always empty.
|
|
||||||
id: :class:`int`
|
|
||||||
The message ID.
|
|
||||||
webhook_id: Optional[:class:`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: :class:`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.
|
|
||||||
activity: Optional[:class:`dict`]
|
|
||||||
The activity associated with this message. Sent with Rich-Presence related messages that for
|
|
||||||
example, request joining, spectating, or listening to or with another member.
|
|
||||||
|
|
||||||
It is a dictionary with the following optional keys:
|
|
||||||
|
|
||||||
- ``type``: An integer denoting the type of message activity being requested.
|
|
||||||
- ``party_id``: The party ID associated with the party.
|
|
||||||
application: Optional[:class:`dict`]
|
|
||||||
The rich presence enabled application associated with this message.
|
|
||||||
|
|
||||||
It is a dictionary with the following keys:
|
|
||||||
|
|
||||||
- ``id``: A string representing the application's ID.
|
|
||||||
- ``name``: A string representing the application's name.
|
|
||||||
- ``description``: A string representing the application's description.
|
|
||||||
- ``icon``: A string representing the icon ID of the application.
|
|
||||||
- ``cover_image``: A string representing the embed's image asset ID.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__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",
|
|
||||||
"application",
|
|
||||||
"activity",
|
|
||||||
)
|
|
||||||
|
|
||||||
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.application = data.get("application")
|
|
||||||
self.activity = data.get("activity")
|
|
||||||
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, "application")
|
|
||||||
self._try_patch(data, "activity")
|
|
||||||
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 filter(None, 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 = self.guild.get_role(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 to 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(self.guild.get_channel, 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
|
|
||||||
|
|
||||||
@property
|
|
||||||
def jump_url(self):
|
|
||||||
""":class:`str`: Returns a URL that allows the client to jump to this message."""
|
|
||||||
guild_id = getattr(self.guild, "id", "@me")
|
|
||||||
return "https://discordapp.com/channels/{0}/{1.channel.id}/{1.id}".format(guild_id, self)
|
|
||||||
|
|
||||||
@utils.cached_slot_property("_cs_system_content")
|
|
||||||
def system_content(self):
|
|
||||||
r"""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)
|
|
||||||
|
|
||||||
async 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.
|
|
||||||
"""
|
|
||||||
await self._state.http.delete_message(self.channel.id, self.id)
|
|
||||||
|
|
||||||
async 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 = await 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:
|
|
||||||
|
|
||||||
async def delete():
|
|
||||||
await asyncio.sleep(delete_after, loop=self._state.loop)
|
|
||||||
try:
|
|
||||||
await self._state.http.delete_message(self.channel.id, self.id)
|
|
||||||
except HTTPException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
asyncio.ensure_future(delete(), loop=self._state.loop)
|
|
||||||
|
|
||||||
async def pin(self):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Pins the message.
|
|
||||||
|
|
||||||
You must have the :attr:`~Permissions.manage_messages` permission 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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
await self._state.http.pin_message(self.channel.id, self.id)
|
|
||||||
self.pinned = True
|
|
||||||
|
|
||||||
async def unpin(self):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Unpins the message.
|
|
||||||
|
|
||||||
You must have the :attr:`~Permissions.manage_messages` permission 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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
await self._state.http.unpin_message(self.channel.id, self.id)
|
|
||||||
self.pinned = False
|
|
||||||
|
|
||||||
async 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.read_message_history` permission
|
|
||||||
to use this. If nobody else has reacted to the message using this
|
|
||||||
emoji, the :attr:`~Permissions.add_reactions` permission is required.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
------------
|
|
||||||
emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialEmoji`, 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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
emoji = self._emoji_reaction(emoji)
|
|
||||||
await self._state.http.add_reaction(self.id, self.channel.id, emoji)
|
|
||||||
|
|
||||||
async 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:`PartialEmoji`, 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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
emoji = self._emoji_reaction(emoji)
|
|
||||||
|
|
||||||
if member.id == self._state.self_id:
|
|
||||||
await self._state.http.remove_own_reaction(self.id, self.channel.id, emoji)
|
|
||||||
else:
|
|
||||||
await self._state.http.remove_reaction(self.id, self.channel.id, emoji, member.id)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _emoji_reaction(emoji):
|
|
||||||
if isinstance(emoji, Reaction):
|
|
||||||
emoji = emoji.emoji
|
|
||||||
|
|
||||||
if isinstance(emoji, Emoji):
|
|
||||||
return "%s:%s" % (emoji.name, emoji.id)
|
|
||||||
if isinstance(emoji, PartialEmoji):
|
|
||||||
return emoji._as_reaction()
|
|
||||||
if isinstance(emoji, str):
|
|
||||||
return emoji # this is okay
|
|
||||||
|
|
||||||
raise InvalidArgument(
|
|
||||||
"emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.".format(
|
|
||||||
emoji
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def clear_reactions(self):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Removes all the reactions from the message.
|
|
||||||
|
|
||||||
You need the :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.
|
|
||||||
"""
|
|
||||||
await 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)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 : :class:`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)
|
|
||||||
286
discord/opus.py
286
discord/opus.py
@@ -1,286 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 array
|
|
||||||
import ctypes
|
|
||||||
import ctypes.util
|
|
||||||
import logging
|
|
||||||
import os.path
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from .errors import DiscordException
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def _err_lt(result, func, args):
|
|
||||||
if result < 0:
|
|
||||||
log.info("error has happened in %s", func.__name__)
|
|
||||||
raise OpusError(result)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _err_ne(result, func, args):
|
|
||||||
ret = args[-1]._obj
|
|
||||||
if ret.value != 0:
|
|
||||||
log.info("error has happened in %s", func.__name__)
|
|
||||||
raise OpusError(ret.value)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
# The fourth is the error handler.
|
|
||||||
exported_functions = [
|
|
||||||
("opus_strerror", [ctypes.c_int], ctypes.c_char_p, None),
|
|
||||||
("opus_encoder_get_size", [ctypes.c_int], ctypes.c_int, None),
|
|
||||||
(
|
|
||||||
"opus_encoder_create",
|
|
||||||
[ctypes.c_int, ctypes.c_int, ctypes.c_int, c_int_ptr],
|
|
||||||
EncoderStructPtr,
|
|
||||||
_err_ne,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"opus_encode",
|
|
||||||
[EncoderStructPtr, c_int16_ptr, ctypes.c_int, ctypes.c_char_p, ctypes.c_int32],
|
|
||||||
ctypes.c_int32,
|
|
||||||
_err_lt,
|
|
||||||
),
|
|
||||||
("opus_encoder_ctl", None, ctypes.c_int32, _err_lt),
|
|
||||||
("opus_encoder_destroy", [EncoderStructPtr], None, None),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def libopus_loader(name):
|
|
||||||
# create the library...
|
|
||||||
lib = ctypes.cdll.LoadLibrary(name)
|
|
||||||
|
|
||||||
# register the functions...
|
|
||||||
for item in exported_functions:
|
|
||||||
func = getattr(lib, item[0])
|
|
||||||
|
|
||||||
try:
|
|
||||||
if item[1]:
|
|
||||||
func.argtypes = item[1]
|
|
||||||
|
|
||||||
func.restype = item[2]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
if item[3]:
|
|
||||||
func.errcheck = item[3]
|
|
||||||
except KeyError:
|
|
||||||
log.exception("Error assigning check function to %s", func)
|
|
||||||
|
|
||||||
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:
|
|
||||||
_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 : :class:`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()
|
|
||||||
return _lib.opus_encoder_create(
|
|
||||||
self.SAMPLING_RATE, self.CHANNELS, self.application, ctypes.byref(ret)
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_bitrate(self, kbps):
|
|
||||||
kbps = min(128, max(16, int(kbps)))
|
|
||||||
|
|
||||||
_lib.opus_encoder_ctl(self._state, CTL_SET_BITRATE, kbps * 1024)
|
|
||||||
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]
|
|
||||||
_lib.opus_encoder_ctl(self._state, CTL_SET_BANDWIDTH, k)
|
|
||||||
|
|
||||||
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]
|
|
||||||
_lib.opus_encoder_ctl(self._state, CTL_SET_SIGNAL, k)
|
|
||||||
|
|
||||||
def set_fec(self, enabled=True):
|
|
||||||
_lib.opus_encoder_ctl(self._state, CTL_SET_FEC, 1 if enabled else 0)
|
|
||||||
|
|
||||||
def set_expected_packet_loss_percent(self, percentage):
|
|
||||||
_lib.opus_encoder_ctl(self._state, CTL_SET_PLP, min(100, max(0, int(percentage * 100))))
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
return array.array("b", data[:ret]).tobytes()
|
|
||||||
@@ -1,643 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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(0b01111111111101111111110111111111)
|
|
||||||
|
|
||||||
@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_nickname
|
|
||||||
- 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(0b00000011111100000000000100000000)
|
|
||||||
|
|
||||||
def update(self, **kwargs):
|
|
||||||
r"""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 is True:
|
|
||||||
self.value |= 1 << index
|
|
||||||
elif value is 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)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def priority_speaker(self):
|
|
||||||
"""Returns True if a user can be more easily heard while talking."""
|
|
||||||
return self._bit(8)
|
|
||||||
|
|
||||||
@priority_speaker.setter
|
|
||||||
def priority_speaker(self, value):
|
|
||||||
self._set(8, value)
|
|
||||||
|
|
||||||
# 1 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 or @here 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:
|
|
||||||
r"""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 |
|
|
||||||
+===========+==========================================+
|
|
||||||
| x == y | Checks if two overwrites are equal. |
|
|
||||||
+-----------+------------------------------------------+
|
|
||||||
| x != y | Checks if two overwrites are not equal. |
|
|
||||||
+-----------+------------------------------------------+
|
|
||||||
| 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 __eq__(self, other):
|
|
||||||
return self._values == other._values
|
|
||||||
|
|
||||||
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):
|
|
||||||
r"""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)
|
|
||||||
@@ -1,369 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 asyncio
|
|
||||||
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
|
|
||||||
:term:`py: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")
|
|
||||||
|
|
||||||
self._process = None
|
|
||||||
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 exc:
|
|
||||||
raise ClientException("Popen failed: {0.__class__.__name__}: {0}".format(exc)) from exc
|
|
||||||
|
|
||||||
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
|
|
||||||
self._speak(True)
|
|
||||||
|
|
||||||
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 exc:
|
|
||||||
self._current_error = exc
|
|
||||||
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 Exception:
|
|
||||||
log.exception("Calling the after function failed.")
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self._end.set()
|
|
||||||
self._resumed.set()
|
|
||||||
self._speak(False)
|
|
||||||
|
|
||||||
def pause(self, *, update_speaking=True):
|
|
||||||
self._resumed.clear()
|
|
||||||
if update_speaking:
|
|
||||||
self._speak(False)
|
|
||||||
|
|
||||||
def resume(self, *, update_speaking=True):
|
|
||||||
self.loops = 0
|
|
||||||
self._start = time.time()
|
|
||||||
self._resumed.set()
|
|
||||||
if update_speaking:
|
|
||||||
self._speak(True)
|
|
||||||
|
|
||||||
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(update_speaking=False)
|
|
||||||
self.source = source
|
|
||||||
self.resume(update_speaking=False)
|
|
||||||
|
|
||||||
def _speak(self, speaking):
|
|
||||||
try:
|
|
||||||
asyncio.run_coroutine_threadsafe(self.client.ws.speak(speaking), self.client.loop)
|
|
||||||
except Exception as e:
|
|
||||||
log.info("Speaking call in player failed: %s", e)
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 RawMessageDeleteEvent:
|
|
||||||
"""Represents the event payload for a :func:`on_raw_message_delete` event.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
------------
|
|
||||||
channel_id: :class:`int`
|
|
||||||
The channel ID where the deletion took place.
|
|
||||||
guild_id: Optional[:class:`int`]
|
|
||||||
The guild ID where the deletion took place, if applicable.
|
|
||||||
message_id: :class:`int`
|
|
||||||
The message ID that got deleted.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("message_id", "channel_id", "guild_id")
|
|
||||||
|
|
||||||
def __init__(self, data):
|
|
||||||
self.message_id = int(data["id"])
|
|
||||||
self.channel_id = int(data["channel_id"])
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.guild_id = int(data["guild_id"])
|
|
||||||
except KeyError:
|
|
||||||
self.guild_id = None
|
|
||||||
|
|
||||||
|
|
||||||
class RawBulkMessageDeleteEvent:
|
|
||||||
"""Represents the event payload for a :func:`on_raw_bulk_message_delete` event.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
-----------
|
|
||||||
message_ids: Set[:class:`int`]
|
|
||||||
A :class:`set` of the message IDs that were deleted.
|
|
||||||
channel_id: :class:`int`
|
|
||||||
The channel ID where the message got deleted.
|
|
||||||
guild_id: Optional[:class:`int`]
|
|
||||||
The guild ID where the message got deleted, if applicable.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("message_ids", "channel_id", "guild_id")
|
|
||||||
|
|
||||||
def __init__(self, data):
|
|
||||||
self.message_ids = {int(x) for x in data.get("ids", [])}
|
|
||||||
self.channel_id = int(data["channel_id"])
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.guild_id = int(data["guild_id"])
|
|
||||||
except KeyError:
|
|
||||||
self.guild_id = None
|
|
||||||
|
|
||||||
|
|
||||||
class RawMessageUpdateEvent:
|
|
||||||
"""Represents the payload for a :func:`on_raw_message_edit` event.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
-----------
|
|
||||||
message_id: :class:`int`
|
|
||||||
The message ID that got updated.
|
|
||||||
data: :class:`dict`
|
|
||||||
The raw data given by the
|
|
||||||
`gateway <https://discordapp.com/developers/docs/topics/gateway#message-update>`_
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("message_id", "data")
|
|
||||||
|
|
||||||
def __init__(self, data):
|
|
||||||
self.message_id = int(data["id"])
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
|
|
||||||
class RawReactionActionEvent:
|
|
||||||
"""Represents the payload for a :func:`on_raw_reaction_add` or
|
|
||||||
:func:`on_raw_reaction_remove` event.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
-----------
|
|
||||||
message_id: :class:`int`
|
|
||||||
The message ID that got or lost a reaction.
|
|
||||||
user_id: :class:`int`
|
|
||||||
The user ID who added the reaction or whose reaction was removed.
|
|
||||||
channel_id: :class:`int`
|
|
||||||
The channel ID where the reaction got added or removed.
|
|
||||||
guild_id: Optional[:class:`int`]
|
|
||||||
The guild ID where the reaction got added or removed, if applicable.
|
|
||||||
emoji: :class:`PartialEmoji`
|
|
||||||
The custom or unicode emoji being used.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("message_id", "user_id", "channel_id", "guild_id", "emoji")
|
|
||||||
|
|
||||||
def __init__(self, data, emoji):
|
|
||||||
self.message_id = int(data["message_id"])
|
|
||||||
self.channel_id = int(data["channel_id"])
|
|
||||||
self.user_id = int(data["user_id"])
|
|
||||||
self.emoji = emoji
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.guild_id = int(data["guild_id"])
|
|
||||||
except KeyError:
|
|
||||||
self.guild_id = None
|
|
||||||
|
|
||||||
|
|
||||||
class RawReactionClearEvent:
|
|
||||||
"""Represents the payload for a :func:`on_raw_reaction_clear` event.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
-----------
|
|
||||||
message_id: :class:`int`
|
|
||||||
The message ID that got its reactions cleared.
|
|
||||||
channel_id: :class:`int`
|
|
||||||
The channel ID where the reactions got cleared.
|
|
||||||
guild_id: Optional[:class:`int`]
|
|
||||||
The guild ID where the reactions got cleared.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("message_id", "channel_id", "guild_id")
|
|
||||||
|
|
||||||
def __init__(self, data):
|
|
||||||
self.message_id = int(data["message_id"])
|
|
||||||
self.channel_id = int(data["channel_id"])
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.guild_id = int(data["guild_id"])
|
|
||||||
except KeyError:
|
|
||||||
self.guild_id = None
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 :class:`str`
|
|
||||||
The reaction emoji. May be a custom emoji, or a unicode emoji.
|
|
||||||
count: :class:`int`
|
|
||||||
Number of times this reaction was made
|
|
||||||
me: :class:`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):
|
|
||||||
""":class:`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):
|
|
||||||
"""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))
|
|
||||||
|
|
||||||
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)
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
async def delete(self):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Deletes the relationship.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
HTTPException
|
|
||||||
Deleting the relationship failed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
await self._state.http.remove_relationship(self.user.id)
|
|
||||||
|
|
||||||
async def accept(self):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Accepts the relationship request. e.g. accepting a
|
|
||||||
friend request.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
-------
|
|
||||||
HTTPException
|
|
||||||
Accepting the relationship failed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
await self._state.http.add_relationship(self.user.id)
|
|
||||||
297
discord/role.py
297
discord/role.py
@@ -1,297 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 .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: :class:`int`
|
|
||||||
The ID for the role.
|
|
||||||
name: :class:`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: :class:`bool`
|
|
||||||
Indicates if the role will be displayed separately from other members.
|
|
||||||
position: :class:`int`
|
|
||||||
The position of the role. This number is usually positive. The bottom
|
|
||||||
role has a position of 0.
|
|
||||||
managed: :class:`bool`
|
|
||||||
Indicates if the role is managed by the guild through some form of
|
|
||||||
integrations such as Twitch.
|
|
||||||
mentionable: :class:`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.")
|
|
||||||
|
|
||||||
# the @everyone role is always the lowest role in hierarchy
|
|
||||||
guild_id = self.guild.id
|
|
||||||
if self.id == guild_id:
|
|
||||||
# everyone_role < everyone_role -> False
|
|
||||||
return other.id != guild_id
|
|
||||||
|
|
||||||
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 :class:`list` of :class:`Member` with this role."""
|
|
||||||
all_members = self.guild.members
|
|
||||||
if self.is_default():
|
|
||||||
return all_members
|
|
||||||
|
|
||||||
role_id = self.id
|
|
||||||
return [member for member in all_members if member._roles.has(role_id)]
|
|
||||||
|
|
||||||
async 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)
|
|
||||||
roles = [
|
|
||||||
r.id for r in self.guild.roles[1:] if r.position in change_range and r.id != self.id
|
|
||||||
]
|
|
||||||
|
|
||||||
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)]
|
|
||||||
await http.move_role_position(self.guild.id, payload, reason=reason)
|
|
||||||
|
|
||||||
async 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:
|
|
||||||
await 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 = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload)
|
|
||||||
self._update(data)
|
|
||||||
|
|
||||||
async 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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
await self._state.http.delete_role(self.guild.id, self.id, reason=reason)
|
|
||||||
370
discord/shard.py
370
discord/shard.py
@@ -1,370 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 logging
|
|
||||||
|
|
||||||
import websockets
|
|
||||||
|
|
||||||
from .state import AutoShardedConnectionState
|
|
||||||
from .client import Client
|
|
||||||
from .gateway import *
|
|
||||||
from .errors import ClientException, InvalidArgument
|
|
||||||
from . import utils
|
|
||||||
from .enums import Status
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Shard:
|
|
||||||
def __init__(self, ws, client):
|
|
||||||
self.ws = ws
|
|
||||||
self._client = client
|
|
||||||
self.loop = self._client.loop
|
|
||||||
self._current = self.loop.create_future()
|
|
||||||
self._current.set_result(None) # we just need an already done future
|
|
||||||
self._pending = asyncio.Event(loop=self.loop)
|
|
||||||
self._pending_task = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def id(self):
|
|
||||||
return self.ws.shard_id
|
|
||||||
|
|
||||||
def is_pending(self):
|
|
||||||
return not self._pending.is_set()
|
|
||||||
|
|
||||||
def complete_pending_reads(self):
|
|
||||||
self._pending.set()
|
|
||||||
|
|
||||||
async def _pending_reads(self):
|
|
||||||
try:
|
|
||||||
while self.is_pending():
|
|
||||||
await self.poll()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def launch_pending_reads(self):
|
|
||||||
self._pending_task = asyncio.ensure_future(self._pending_reads(), loop=self.loop)
|
|
||||||
|
|
||||||
def wait(self):
|
|
||||||
return self._pending_task
|
|
||||||
|
|
||||||
async def poll(self):
|
|
||||||
try:
|
|
||||||
await self.ws.poll_event()
|
|
||||||
except ResumeWebSocket:
|
|
||||||
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 = await asyncio.wait_for(coro, timeout=180.0, loop=self.loop)
|
|
||||||
|
|
||||||
def get_future(self):
|
|
||||||
if self._current.done():
|
|
||||||
self._current = asyncio.ensure_future(self.poll(), loop=self.loop)
|
|
||||||
|
|
||||||
return self._current
|
|
||||||
|
|
||||||
|
|
||||||
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[:class:`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,
|
|
||||||
handlers=self._handlers,
|
|
||||||
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
|
|
||||||
|
|
||||||
async 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
|
|
||||||
await ws.send_as_json(payload)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latency(self):
|
|
||||||
""":class:`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. Returns ``nan`` if there are no shards ready.
|
|
||||||
"""
|
|
||||||
if not self.shards:
|
|
||||||
return float("nan")
|
|
||||||
return sum(latency for _, latency in self.latencies) / len(self.shards)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latencies(self):
|
|
||||||
"""List[Tuple[:class:`int`, :class:`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()]
|
|
||||||
|
|
||||||
async def request_offline_members(self, *guilds):
|
|
||||||
r"""|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)
|
|
||||||
await self._connection.request_offline_members(sub_guilds, shard_id=shard_id)
|
|
||||||
|
|
||||||
async def launch_shard(self, gateway, shard_id):
|
|
||||||
try:
|
|
||||||
coro = websockets.connect(
|
|
||||||
gateway, loop=self.loop, klass=DiscordWebSocket, compression=None
|
|
||||||
)
|
|
||||||
ws = await asyncio.wait_for(coro, loop=self.loop, timeout=180.0)
|
|
||||||
except Exception:
|
|
||||||
log.info("Failed to connect for shard_id: %s. Retrying...", shard_id)
|
|
||||||
await asyncio.sleep(5.0, loop=self.loop)
|
|
||||||
return await 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
|
|
||||||
await asyncio.wait_for(ws.poll_event(), loop=self.loop, timeout=180.0)
|
|
||||||
await 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)
|
|
||||||
await asyncio.sleep(5.0, loop=self.loop)
|
|
||||||
return await self.launch_shard(gateway, shard_id)
|
|
||||||
|
|
||||||
# keep reading the shard while others connect
|
|
||||||
self.shards[shard_id] = ret = Shard(ws, self)
|
|
||||||
ret.launch_pending_reads()
|
|
||||||
await asyncio.sleep(5.0, loop=self.loop)
|
|
||||||
|
|
||||||
async def launch_shards(self):
|
|
||||||
if self.shard_count is None:
|
|
||||||
self.shard_count, gateway = await self.http.get_bot_gateway()
|
|
||||||
else:
|
|
||||||
gateway = await 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:
|
|
||||||
await self.launch_shard(gateway, shard_id)
|
|
||||||
|
|
||||||
shards_to_wait_for = []
|
|
||||||
for shard in self.shards.values():
|
|
||||||
shard.complete_pending_reads()
|
|
||||||
shards_to_wait_for.append(shard.wait())
|
|
||||||
|
|
||||||
# wait for all pending tasks to finish
|
|
||||||
await utils.sane_wait_for(shards_to_wait_for, timeout=300.0, loop=self.loop)
|
|
||||||
|
|
||||||
async def _connect(self):
|
|
||||||
await self.launch_shards()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
pollers = [shard.get_future() for shard in self.shards.values()]
|
|
||||||
done, _ = await 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()
|
|
||||||
|
|
||||||
async def close(self):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Closes the connection to discord.
|
|
||||||
"""
|
|
||||||
if self.is_closed():
|
|
||||||
return
|
|
||||||
|
|
||||||
self._closed.set()
|
|
||||||
|
|
||||||
for vc in self.voice_clients:
|
|
||||||
try:
|
|
||||||
await vc.disconnect()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
to_close = [shard.ws.close() for shard in self.shards.values()]
|
|
||||||
if to_close:
|
|
||||||
await asyncio.wait(to_close, loop=self.loop)
|
|
||||||
|
|
||||||
await self.http.close()
|
|
||||||
|
|
||||||
async def change_presence(self, *, activity=None, status=None, afk=False, shard_id=None):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Changes the client's presence.
|
|
||||||
|
|
||||||
The activity parameter is a :class:`Activity` object (not a string) that represents
|
|
||||||
the activity being done currently. This could also be the slimmed down versions,
|
|
||||||
:class:`Game` and :class:`Streaming`.
|
|
||||||
|
|
||||||
Example: ::
|
|
||||||
|
|
||||||
game = discord.Game("with the API")
|
|
||||||
await client.change_presence(status=discord.Status.idle, activity=game)
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
activity: Optional[Union[:class:`Game`, :class:`Streaming`, :class:`Activity`]]
|
|
||||||
The activity being done. ``None`` if no currently active activity is done.
|
|
||||||
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 ``activity`` parameter is not of proper type.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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():
|
|
||||||
await shard.ws.change_presence(activity=activity, status=status, afk=afk)
|
|
||||||
|
|
||||||
guilds = self._connection.guilds
|
|
||||||
else:
|
|
||||||
shard = self.shards[shard_id]
|
|
||||||
await shard.ws.change_presence(activity=activity, 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.activities = (activity,)
|
|
||||||
me.status = status_enum
|
|
||||||
1048
discord/state.py
1048
discord/state.py
File diff suppressed because it is too large
Load Diff
699
discord/user.py
699
discord/user.py
@@ -1,699 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2017 Rapptz
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a
|
|
||||||
copy of this software and associated documentation files (the "Software"),
|
|
||||||
to deal in the Software without restriction, including without limitation
|
|
||||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
||||||
and/or sell copies of the Software, and to permit persons to whom the
|
|
||||||
Software is furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
DEALINGS IN THE SOFTWARE.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
import discord.abc
|
|
||||||
from .utils import snowflake_time, _bytes_to_base64_data, parse_time, valid_icon_size
|
|
||||||
from .enums import DefaultAvatar, RelationshipType, UserFlags, HypeSquadHouse
|
|
||||||
from .errors import ClientException, InvalidArgument
|
|
||||||
from .colour import Colour
|
|
||||||
|
|
||||||
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 partner(self):
|
|
||||||
return self._has_flag(UserFlags.partner)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bug_hunter(self):
|
|
||||||
return self._has_flag(UserFlags.bug_hunter)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def early_supporter(self):
|
|
||||||
return self._has_flag(UserFlags.early_supporter)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hypesquad(self):
|
|
||||||
return self._has_flag(UserFlags.hypesquad)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hypesquad_houses(self):
|
|
||||||
flags = (
|
|
||||||
UserFlags.hypesquad_bravery,
|
|
||||||
UserFlags.hypesquad_brilliance,
|
|
||||||
UserFlags.hypesquad_balance,
|
|
||||||
)
|
|
||||||
return [house for house, flag in zip(HypeSquadHouse, flags) if self._has_flag(flag)]
|
|
||||||
|
|
||||||
|
|
||||||
_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
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _copy(cls, user):
|
|
||||||
self = cls.__new__(cls) # bypass __init__
|
|
||||||
|
|
||||||
self.name = user.name
|
|
||||||
self.id = user.id
|
|
||||||
self.discriminator = user.discriminator
|
|
||||||
self.avatar = user.avatar
|
|
||||||
self.bot = user.bot
|
|
||||||
self._state = user._state
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
@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):
|
|
||||||
""":class:`bool`: Returns True if the user has an animated avatar."""
|
|
||||||
return bool(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 discriminator"""
|
|
||||||
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 colour(self):
|
|
||||||
"""A property that returns a :class:`Colour` denoting the rendered colour
|
|
||||||
for the user. This always returns :meth:`Colour.default`.
|
|
||||||
|
|
||||||
There is an alias for this under ``color``.
|
|
||||||
"""
|
|
||||||
return Colour.default()
|
|
||||||
|
|
||||||
color = colour
|
|
||||||
|
|
||||||
@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: :class:`str`
|
|
||||||
The user's username.
|
|
||||||
id: :class:`int`
|
|
||||||
The user's unique ID.
|
|
||||||
discriminator: :class:`str`
|
|
||||||
The user's discriminator. This is given when the username has conflicts.
|
|
||||||
avatar: Optional[:class:`str`]
|
|
||||||
The avatar hash the user has. Could be None.
|
|
||||||
bot: :class:`bool`
|
|
||||||
Specifies if the user is a bot account.
|
|
||||||
verified: :class:`bool`
|
|
||||||
Specifies if the user is a verified account.
|
|
||||||
email: Optional[:class:`str`]
|
|
||||||
The email the user used when registering.
|
|
||||||
mfa_enabled: :class:`bool`
|
|
||||||
Specifies if the user has MFA turned on and working.
|
|
||||||
premium: :class:`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 :class:`list` of :class:`Relationship` that the user has."""
|
|
||||||
return list(self._relationships.values())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def friends(self):
|
|
||||||
r"""Returns a :class:`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):
|
|
||||||
r"""Returns a :class:`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]
|
|
||||||
|
|
||||||
async 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 :term:`py: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 :term:`py: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.
|
|
||||||
house: Optional[:class:`HypeSquadHouse`]
|
|
||||||
The hypesquad house you wish to change to.
|
|
||||||
Could be ``None`` to leave the current house.
|
|
||||||
Only applicable to user accounts.
|
|
||||||
username :str
|
|
||||||
The new username you wish to change to.
|
|
||||||
avatar: bytes
|
|
||||||
A :term:`py: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.
|
|
||||||
House field was not a HypeSquadHouse.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
if "house" in fields:
|
|
||||||
house = fields["house"]
|
|
||||||
if house is None:
|
|
||||||
await http.leave_hypesquad_house()
|
|
||||||
elif not isinstance(house, HypeSquadHouse):
|
|
||||||
raise ClientException("`house` parameter was not a HypeSquadHouse")
|
|
||||||
else:
|
|
||||||
value = house.value
|
|
||||||
|
|
||||||
await http.change_hypesquad_house(value)
|
|
||||||
|
|
||||||
data = await 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)
|
|
||||||
|
|
||||||
async def create_group(self, *recipients):
|
|
||||||
r"""|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 :class:`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 = await self._state.http.start_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: :class:`str`
|
|
||||||
The user's username.
|
|
||||||
id: :class:`int`
|
|
||||||
The user's unique ID.
|
|
||||||
discriminator: :class:`str`
|
|
||||||
The user's discriminator. This is given when the username has conflicts.
|
|
||||||
avatar: Optional[:class:`str`]
|
|
||||||
The avatar hash the user has. Could be None.
|
|
||||||
bot: :class:`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
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _get_channel(self):
|
|
||||||
ch = await 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)
|
|
||||||
|
|
||||||
async 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 = await 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)
|
|
||||||
|
|
||||||
async def mutual_friends(self):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Gets all mutual friends of this user. This can only be used by non-bot accounts
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
List[:class:`User`]
|
|
||||||
The users that are mutual friends.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
-------
|
|
||||||
Forbidden
|
|
||||||
Not allowed to get mutual friends of this user.
|
|
||||||
HTTPException
|
|
||||||
Getting mutual friends failed.
|
|
||||||
"""
|
|
||||||
state = self._state
|
|
||||||
mutuals = await state.http.get_mutual_friends(self.id)
|
|
||||||
return [User(state=state, data=friend) for friend in mutuals]
|
|
||||||
|
|
||||||
def is_friend(self):
|
|
||||||
""":class:`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):
|
|
||||||
""":class:`bool`: Checks if the user is blocked."""
|
|
||||||
r = self.relationship
|
|
||||||
if r is None:
|
|
||||||
return False
|
|
||||||
return r.type is RelationshipType.blocked
|
|
||||||
|
|
||||||
async def block(self):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Blocks the user.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
-------
|
|
||||||
Forbidden
|
|
||||||
Not allowed to block this user.
|
|
||||||
HTTPException
|
|
||||||
Blocking the user failed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
await self._state.http.add_relationship(self.id, type=RelationshipType.blocked.value)
|
|
||||||
|
|
||||||
async def unblock(self):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Unblocks the user.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
-------
|
|
||||||
Forbidden
|
|
||||||
Not allowed to unblock this user.
|
|
||||||
HTTPException
|
|
||||||
Unblocking the user failed.
|
|
||||||
"""
|
|
||||||
await self._state.http.remove_relationship(self.id)
|
|
||||||
|
|
||||||
async 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.
|
|
||||||
"""
|
|
||||||
await self._state.http.remove_relationship(self.id)
|
|
||||||
|
|
||||||
async 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.
|
|
||||||
"""
|
|
||||||
await self._state.http.send_friend_request(
|
|
||||||
username=self.name, discriminator=self.discriminator
|
|
||||||
)
|
|
||||||
|
|
||||||
async 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 = await 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"],
|
|
||||||
)
|
|
||||||
369
discord/utils.py
369
discord/utils.py
@@ -1,369 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 array
|
|
||||||
import asyncio
|
|
||||||
import unicodedata
|
|
||||||
from base64 import b64encode
|
|
||||||
from bisect import bisect_left
|
|
||||||
import datetime
|
|
||||||
from email.utils import parsedate_to_datetime
|
|
||||||
import functools
|
|
||||||
from inspect import isawaitable as _isawaitable
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from .errors import InvalidArgument
|
|
||||||
|
|
||||||
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=" + str(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):
|
|
||||||
r"""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", b"\x47\x49\x46\x38\x39\x61")):
|
|
||||||
return "image/gif"
|
|
||||||
elif data.startswith(b"RIFF") and data[8:12] == b"WEBP":
|
|
||||||
return "image/webp"
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
async def maybe_coroutine(f, *args, **kwargs):
|
|
||||||
value = f(*args, **kwargs)
|
|
||||||
if _isawaitable(value):
|
|
||||||
return await value
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
async def async_all(gen, *, check=_isawaitable):
|
|
||||||
for elem in gen:
|
|
||||||
if check(elem):
|
|
||||||
elem = await elem
|
|
||||||
if not elem:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def sane_wait_for(futures, *, timeout, loop):
|
|
||||||
_, pending = await 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, 2048]."""
|
|
||||||
return not size & (size - 1) and size in range(16, 2049)
|
|
||||||
|
|
||||||
|
|
||||||
class SnowflakeList(array.array):
|
|
||||||
"""Internal data storage class to efficiently store a list of snowflakes.
|
|
||||||
|
|
||||||
This should have the following characteristics:
|
|
||||||
|
|
||||||
- Low memory usage
|
|
||||||
- O(n) iteration (obviously)
|
|
||||||
- O(n log n) initial creation if data is unsorted
|
|
||||||
- O(log n) search and indexing
|
|
||||||
- O(n) insertion
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def __new__(cls, data, *, is_sorted=False):
|
|
||||||
return array.array.__new__(cls, "Q", data if is_sorted else sorted(data))
|
|
||||||
|
|
||||||
def add(self, element):
|
|
||||||
i = bisect_left(self, element)
|
|
||||||
self.insert(i, element)
|
|
||||||
|
|
||||||
def get(self, element):
|
|
||||||
i = bisect_left(self, element)
|
|
||||||
return self[i] if i != len(self) and self[i] == element else None
|
|
||||||
|
|
||||||
def has(self, element):
|
|
||||||
i = bisect_left(self, element)
|
|
||||||
return i != len(self) and self[i] == element
|
|
||||||
|
|
||||||
|
|
||||||
_IS_ASCII = re.compile(r"^[\x00-\x7f]+$")
|
|
||||||
|
|
||||||
|
|
||||||
def _string_width(string, *, _IS_ASCII=_IS_ASCII):
|
|
||||||
"""Returns string's width."""
|
|
||||||
match = _IS_ASCII.match(string)
|
|
||||||
if match:
|
|
||||||
return match.endpos
|
|
||||||
|
|
||||||
UNICODE_WIDE_CHAR_TYPE = "WFA"
|
|
||||||
width = 0
|
|
||||||
func = unicodedata.east_asian_width
|
|
||||||
for char in string:
|
|
||||||
width += 2 if func(char) in UNICODE_WIDE_CHAR_TYPE else 1
|
|
||||||
return width
|
|
||||||
@@ -1,448 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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
|
|
||||||
|
|
||||||
from . import opus
|
|
||||||
from .backoff import ExponentialBackoff
|
|
||||||
from .gateway import *
|
|
||||||
from .errors import ClientException, ConnectionClosed
|
|
||||||
from .player import AudioPlayer, AudioSource
|
|
||||||
|
|
||||||
try:
|
|
||||||
import nacl.secret
|
|
||||||
|
|
||||||
has_nacl = True
|
|
||||||
except ImportError:
|
|
||||||
has_nacl = False
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
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: :class:`str`
|
|
||||||
The voice connection session ID.
|
|
||||||
token: :class:`str`
|
|
||||||
The voice connection token.
|
|
||||||
endpoint: :class:`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.mode = None
|
|
||||||
self._connections = 0
|
|
||||||
self.sequence = 0
|
|
||||||
self.timestamp = 0
|
|
||||||
self._runner = None
|
|
||||||
self._player = None
|
|
||||||
self.encoder = opus.Encoder()
|
|
||||||
|
|
||||||
warn_nacl = not has_nacl
|
|
||||||
supported_modes = ("xsalsa20_poly1305_suffix", "xsalsa20_poly1305")
|
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
async def start_handshake(self):
|
|
||||||
log.info("Starting voice handshake...")
|
|
||||||
|
|
||||||
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
|
|
||||||
await ws.voice_state(guild_id, channel_id)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await asyncio.wait_for(
|
|
||||||
self._handshake_complete.wait(), timeout=self.timeout, loop=self.loop
|
|
||||||
)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
await self.terminate_handshake(remove=True)
|
|
||||||
raise
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"Voice handshake complete. Endpoint found %s (IP: %s)", self.endpoint, self.endpoint_ip
|
|
||||||
)
|
|
||||||
|
|
||||||
async def terminate_handshake(self, *, remove=False):
|
|
||||||
guild_id, channel_id = self.channel._get_voice_state_pair()
|
|
||||||
self._handshake_complete.clear()
|
|
||||||
await 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)
|
|
||||||
|
|
||||||
async 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 Exception:
|
|
||||||
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()
|
|
||||||
await self.ws.close(4000)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._handshake_complete.set()
|
|
||||||
|
|
||||||
async 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:
|
|
||||||
await self.start_handshake()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.ws = await DiscordVoiceWebSocket.from_client(self)
|
|
||||||
self._connected.clear()
|
|
||||||
while not hasattr(self, "secret_key"):
|
|
||||||
await self.ws.poll_event()
|
|
||||||
self._connected.set()
|
|
||||||
except (ConnectionClosed, asyncio.TimeoutError):
|
|
||||||
if reconnect and _tries < 5:
|
|
||||||
log.exception("Failed to connect to voice... Retrying...")
|
|
||||||
await asyncio.sleep(1 + _tries * 2.0, loop=self.loop)
|
|
||||||
await self.terminate_handshake()
|
|
||||||
await 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))
|
|
||||||
|
|
||||||
async def poll_voice_ws(self, reconnect):
|
|
||||||
backoff = ExponentialBackoff()
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
await self.ws.poll_event()
|
|
||||||
except (ConnectionClosed, asyncio.TimeoutError) as exc:
|
|
||||||
if isinstance(exc, ConnectionClosed):
|
|
||||||
if exc.code == 1000:
|
|
||||||
await self.disconnect()
|
|
||||||
break
|
|
||||||
|
|
||||||
if not reconnect:
|
|
||||||
await self.disconnect()
|
|
||||||
raise
|
|
||||||
|
|
||||||
retry = backoff.delay()
|
|
||||||
log.exception("Disconnected from voice... Reconnecting in %.2fs.", retry)
|
|
||||||
self._connected.clear()
|
|
||||||
await asyncio.sleep(retry, loop=self.loop)
|
|
||||||
await self.terminate_handshake()
|
|
||||||
try:
|
|
||||||
await 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
|
|
||||||
|
|
||||||
async 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:
|
|
||||||
await self.ws.close()
|
|
||||||
|
|
||||||
await self.terminate_handshake(remove=True)
|
|
||||||
finally:
|
|
||||||
if self.socket:
|
|
||||||
self.socket.close()
|
|
||||||
|
|
||||||
async 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()
|
|
||||||
await self.main_ws.voice_state(guild_id, channel.id)
|
|
||||||
|
|
||||||
def is_connected(self):
|
|
||||||
""":class:`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)
|
|
||||||
|
|
||||||
# Formulate rtp 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)
|
|
||||||
|
|
||||||
encrypt_packet = getattr(self, "_encrypt_" + self.mode)
|
|
||||||
return encrypt_packet(header, data)
|
|
||||||
|
|
||||||
def _encrypt_xsalsa20_poly1305(self, header, data):
|
|
||||||
box = nacl.secret.SecretBox(bytes(self.secret_key))
|
|
||||||
nonce = bytearray(24)
|
|
||||||
nonce[:12] = header
|
|
||||||
|
|
||||||
return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext
|
|
||||||
|
|
||||||
def _encrypt_xsalsa20_poly1305_suffix(self, header, data):
|
|
||||||
box = nacl.secret.SecretBox(bytes(self.secret_key))
|
|
||||||
nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE)
|
|
||||||
|
|
||||||
return header + box.encrypt(bytes(data), nonce).ciphertext + nonce
|
|
||||||
|
|
||||||
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 :term:`py: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)
|
|
||||||
@@ -1,703 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-2019 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 json
|
|
||||||
import time
|
|
||||||
import re
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
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 :class:`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.webhook._state, data=data)
|
|
||||||
|
|
||||||
def execute_webhook(self, *, payload, wait=False, file=None, files=None):
|
|
||||||
if file is not None:
|
|
||||||
multipart = {"file": file, "payload_json": utils.to_json(payload)}
|
|
||||||
data = None
|
|
||||||
elif files is not None:
|
|
||||||
multipart = {"payload_json": utils.to_json(payload)}
|
|
||||||
for i, file in enumerate(files, start=1):
|
|
||||||
multipart["file%i" % i] = file
|
|
||||||
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
|
|
||||||
|
|
||||||
async 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:
|
|
||||||
data = aiohttp.FormData()
|
|
||||||
for key, value in multipart.items():
|
|
||||||
if key.startswith("file"):
|
|
||||||
data.add_field(key, value[1], filename=value[0], content_type=value[2])
|
|
||||||
else:
|
|
||||||
data.add_field(key, value)
|
|
||||||
|
|
||||||
for tries in range(5):
|
|
||||||
async with self.session.request(verb, url, headers=headers, data=data) as r:
|
|
||||||
data = await 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)
|
|
||||||
await 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
|
|
||||||
await asyncio.sleep(retry_after, loop=self.loop)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if r.status in (500, 502):
|
|
||||||
await 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)
|
|
||||||
|
|
||||||
async def handle_execution_response(self, response, *, wait):
|
|
||||||
data = await response
|
|
||||||
if not wait:
|
|
||||||
return data
|
|
||||||
|
|
||||||
# transform into Message object
|
|
||||||
from .message import Message
|
|
||||||
|
|
||||||
return Message(data=data, state=self.webhook._state, 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)
|
|
||||||
|
|
||||||
if multipart is not None:
|
|
||||||
data = {"payload_json": multipart.pop("payload_json")}
|
|
||||||
|
|
||||||
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.webhook._state, 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: :class:`int`
|
|
||||||
The webhook's ID
|
|
||||||
token: :class:`str`
|
|
||||||
The authentication token of the webhook.
|
|
||||||
guild_id: Optional[:class:`int`]
|
|
||||||
The guild ID this webhook is for.
|
|
||||||
channel_id: Optional[:class:`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[:class:`str`]
|
|
||||||
The default name of the webhook.
|
|
||||||
avatar: Optional[:class:`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()
|
|
||||||
|
|
||||||
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 :term:`py: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,
|
|
||||||
files=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 :class:`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. This cannot be mixed with ``files`` parameter.
|
|
||||||
files: List[:class:`File`]
|
|
||||||
A list of files to send with the content. This cannot be mixed with the
|
|
||||||
``file`` parameter.
|
|
||||||
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 files is not None and file is not None:
|
|
||||||
raise InvalidArgument("Cannot mix file and files keyword arguments.")
|
|
||||||
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.filename, file.open_file(), "application/octet-stream")
|
|
||||||
return self._adapter.execute_webhook(wait=wait, file=to_pass, payload=payload)
|
|
||||||
finally:
|
|
||||||
file.close()
|
|
||||||
elif files is not None:
|
|
||||||
try:
|
|
||||||
to_pass = [
|
|
||||||
(file.filename, file.open_file(), "application/octet-stream") for file in files
|
|
||||||
]
|
|
||||||
return self._adapter.execute_webhook(wait=wait, files=to_pass, payload=payload)
|
|
||||||
finally:
|
|
||||||
for file in files:
|
|
||||||
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)
|
|
||||||
@@ -28,6 +28,7 @@ Paste the following and replace all instances of :code:`username` with the usern
|
|||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=15
|
RestartSec=15
|
||||||
RestartPreventExitStatus=0
|
RestartPreventExitStatus=0
|
||||||
|
TimeoutStopSec=10
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
@@ -48,6 +49,14 @@ To set the bot to start on boot, you must enable the service, again adding the i
|
|||||||
|
|
||||||
:code:`sudo systemctl enable red@instancename`
|
:code:`sudo systemctl enable red@instancename`
|
||||||
|
|
||||||
|
If you need to shutdown the bot, you can use the ``[p]shutdown`` command or
|
||||||
|
type the following command in the terminal, still by adding the instance name after the **@**:
|
||||||
|
|
||||||
|
:code:`sudo systemctl stop red@instancename`
|
||||||
|
|
||||||
|
.. warning:: If the service doesn't stop in the next 10 seconds, the process is killed.
|
||||||
|
Check your logs to know the cause of the error that prevents the shutdown.
|
||||||
|
|
||||||
To view Red’s log, you can acccess through journalctl:
|
To view Red’s log, you can acccess through journalctl:
|
||||||
|
|
||||||
:code:`sudo journalctl -u red@instancename`
|
:code:`sudo journalctl -u red@instancename`
|
||||||
|
|||||||
232
docs/changelog_3_1_0.rst
Normal file
232
docs/changelog_3_1_0.rst
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
.. v3.1.0 Changelog
|
||||||
|
|
||||||
|
####################
|
||||||
|
v3.1.0 Release Notes
|
||||||
|
####################
|
||||||
|
|
||||||
|
----------------------
|
||||||
|
Mongo Driver Migration
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Due to the required changes of the Mongo driver for Config, all existing Mongo users will need to
|
||||||
|
complete the below instructions to continue to use Mongo after updating to 3.1.
|
||||||
|
This includes **all** users, regardless of any prior migration attempt to a development version of
|
||||||
|
3.1.
|
||||||
|
|
||||||
|
#. Upgrade to 3.1
|
||||||
|
#. Convert all existing Mongo instances to JSON using the new converters
|
||||||
|
#. Start each bot instance while using JSON and load any and all cogs you have in order to successfully preserve data.
|
||||||
|
#. Turn each instance off and convert back to Mongo.
|
||||||
|
**NOTE:** No data is wiped from your Mongo database when converting to JSON.
|
||||||
|
You may want to use a *new* database name when converting back to Mongo in order to not have duplicate data.
|
||||||
|
|
||||||
|
-------------
|
||||||
|
Setup Utility
|
||||||
|
-------------
|
||||||
|
|
||||||
|
New commands were introduced to simplify the conversion/editing/removal process both on our end and the users end.
|
||||||
|
Please use ``redbot-setup --help`` to learn how to use the new features.
|
||||||
|
|
||||||
|
.. HINT::
|
||||||
|
|
||||||
|
Converting to JSON: ``redbot-setup convert <instance_name> json``
|
||||||
|
|
||||||
|
Converting to Mongo: ``redbot-setup convert <instance_name> mongo``
|
||||||
|
|
||||||
|
################
|
||||||
|
v3.1.0 Changelog
|
||||||
|
################
|
||||||
|
|
||||||
|
-----
|
||||||
|
Audio
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Add Spotify support (`#2328`_)
|
||||||
|
* Play local folders via text command (`#2457`_)
|
||||||
|
* Change pause to a toggle (`#2461`_)
|
||||||
|
* Remove aliases (`#2462`_)
|
||||||
|
* Add track length restriction (`#2465`_)
|
||||||
|
* Seek command can now seek to position (`#2470`_)
|
||||||
|
* Add option for dc at queue end (`#2472`_)
|
||||||
|
* Emptydisconnect and status refactor (`#2473`_)
|
||||||
|
* Queue clean and queue clear addition (`#2476`_)
|
||||||
|
* Fix for audioset status (`#2481`_)
|
||||||
|
* Playlist download addition (`#2482`_)
|
||||||
|
* Add songs when search-queuing (`#2513`_)
|
||||||
|
* Match v2 behavior for channel change (`#2521`_)
|
||||||
|
* Bot will no longer complain about permissions when trying to connect to user-limited channel, if it has "Move Members" permission (`#2525`_)
|
||||||
|
* Fix issue on audiostats command when more than 20 servers to display (`#2533`_)
|
||||||
|
* Fix for prev command display (`#2556`_)
|
||||||
|
* Fix for localtrack playing (`#2557`_)
|
||||||
|
* Fix for playlist queue when not playing (`#2586`_)
|
||||||
|
* Track search and append fixes (`#2591`_)
|
||||||
|
* DJ role should ask for a role (`#2606`_)
|
||||||
|
|
||||||
|
----
|
||||||
|
Core
|
||||||
|
----
|
||||||
|
|
||||||
|
* Warn on usage of ``yaml.load`` (`#2326`_)
|
||||||
|
* New Event dispatch: ``on_message_without_command`` (`#2338`_)
|
||||||
|
* Improve output format of cooldown messages (`#2412`_)
|
||||||
|
* Delete cooldown messages when expired (`#2469`_)
|
||||||
|
* Fix local blacklist/whitelist management (`#2531`_)
|
||||||
|
* ``[p]set locale`` now only accepts actual locales (`#2553`_)
|
||||||
|
* ``[p]listlocales`` now displays ``en-US`` (`#2553`_)
|
||||||
|
* ``redbot --version`` will now give you current version of Red (`#2567`_)
|
||||||
|
* Redesign help and related formatter (`#2628`_)
|
||||||
|
* Default locale changed from ``en`` to ``en-US`` (`#2642`_)
|
||||||
|
* New command ``[p]datapath`` that prints the bot's datapath (`#2652`_)
|
||||||
|
|
||||||
|
------
|
||||||
|
Config
|
||||||
|
------
|
||||||
|
|
||||||
|
* Updated Mongo driver to support large guilds (`#2536`_)
|
||||||
|
* Introduced ``init_custom`` method on Config objects (`#2545`_)
|
||||||
|
* We now record custom group primary key lengths in the core config object (`#2550`_)
|
||||||
|
* Migrated internal UUIDs to maintain cross platform consistency (`#2604`_)
|
||||||
|
|
||||||
|
-------------
|
||||||
|
DataConverter
|
||||||
|
-------------
|
||||||
|
|
||||||
|
* It's dead jim (Removal) (`#2554`_)
|
||||||
|
|
||||||
|
----------
|
||||||
|
discord.py
|
||||||
|
----------
|
||||||
|
|
||||||
|
* No longer vendoring discord.py (`#2587`_)
|
||||||
|
* Upgraded discord.py dependency to version 1.0.1 (`#2587`_)
|
||||||
|
|
||||||
|
----------
|
||||||
|
Downloader
|
||||||
|
----------
|
||||||
|
|
||||||
|
* ``[p]cog install`` will now tell user that cog has to be loaded (`#2523`_)
|
||||||
|
* The message when libraries fail to install is now formatted (`#2576`_)
|
||||||
|
* Fixed bug, that caused Downloader to include submodules on cog list (`#2590`_)
|
||||||
|
* ``[p]cog uninstall`` allows to uninstall multiple cogs now (`#2592`_)
|
||||||
|
* ``[p]cog uninstall`` will now remove cog from installed cogs even if it can't find the cog in install path anymore (`#2595`_)
|
||||||
|
* ``[p]cog install`` will not allow to install cogs which aren't suitable for installed version of Red anymore (`#2605`_)
|
||||||
|
* Cog Developers now have to use ``min_bot_version`` in form of version string instead of ``bot_version`` in info.json and they can also use ``max_bot_version`` to specify maximum version of Red, more in :doc:`framework_downloader`. (`#2605`_)
|
||||||
|
|
||||||
|
------
|
||||||
|
Filter
|
||||||
|
------
|
||||||
|
|
||||||
|
* Filter performs significantly better on large servers. (`#2509`_)
|
||||||
|
|
||||||
|
--------
|
||||||
|
Launcher
|
||||||
|
--------
|
||||||
|
|
||||||
|
* Fixed extras in the launcher (`#2588`_)
|
||||||
|
|
||||||
|
---
|
||||||
|
Mod
|
||||||
|
---
|
||||||
|
|
||||||
|
* Admins can now decide how many times message has to be repeated before ``deleterepeats`` removes it (`#2437`_)
|
||||||
|
* Fix: make ``[p]ban [days]`` optional as per the doc (`#2602`_)
|
||||||
|
* Added the command ``voicekick`` to kick members from a voice channel with optional mod case. (`#2639`_)
|
||||||
|
|
||||||
|
-----------
|
||||||
|
Permissions
|
||||||
|
-----------
|
||||||
|
|
||||||
|
* Removed: ``p`` alias for ``permissions`` command (`#2467`_)
|
||||||
|
|
||||||
|
-------------
|
||||||
|
Setup Scripts
|
||||||
|
-------------
|
||||||
|
|
||||||
|
* ``redbot-setup`` now uses the click CLI library (`#2579`_)
|
||||||
|
* ``redbot-setup convert`` now used to convert between libraries (`#2579`_)
|
||||||
|
* Backup support for Mongo is currently broken (`#2579`_)
|
||||||
|
|
||||||
|
-------
|
||||||
|
Streams
|
||||||
|
-------
|
||||||
|
|
||||||
|
* Add support for custom stream alert messages per guild (`#2600`_)
|
||||||
|
* Add ability to exclude rerun Twitch streams, and note rerun streams in embed status (`#2620`_)
|
||||||
|
|
||||||
|
-----
|
||||||
|
Tests
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Test for ``trivia`` cog uses explicitly utf-8 encoding for checking yaml files (`#2565`_)
|
||||||
|
|
||||||
|
------
|
||||||
|
Trivia
|
||||||
|
------
|
||||||
|
|
||||||
|
* Fix of dead image link for Sao Tome and Principe in ``worldflags`` trivia (`#2540`_)
|
||||||
|
|
||||||
|
-----------------
|
||||||
|
Utility Functions
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
* New: ``chat_formatting.humanize_timedelta`` (`#2412`_)
|
||||||
|
* ``Tunnel`` - Spelling correction of method name - changed ``files_from_attatch`` to ``files_from_attach`` (old name is left for backwards compatibility) (`#2496`_)
|
||||||
|
* ``Tunnel`` - fixed behavior of ``react_close()``, now when tunnel closes message will be sent to other end (`#2507`_)
|
||||||
|
* ``chat_formatting.humanize_list`` - Improved error handling of empty lists (`#2597`_)
|
||||||
|
|
||||||
|
.. _#2326: https://github.com/Cog-Creators/Red-DiscordBot/pull/2326
|
||||||
|
.. _#2328: https://github.com/Cog-Creators/Red-DiscordBot/pull/2328
|
||||||
|
.. _#2338: https://github.com/Cog-Creators/Red-DiscordBot/pull/2338
|
||||||
|
.. _#2412: https://github.com/Cog-Creators/Red-DiscordBot/pull/2412
|
||||||
|
.. _#2437: https://github.com/Cog-Creators/Red-DiscordBot/pull/2437
|
||||||
|
.. _#2457: https://github.com/Cog-Creators/Red-DiscordBot/pull/2457
|
||||||
|
.. _#2461: https://github.com/Cog-Creators/Red-DiscordBot/pull/2461
|
||||||
|
.. _#2462: https://github.com/Cog-Creators/Red-DiscordBot/pull/2462
|
||||||
|
.. _#2465: https://github.com/Cog-Creators/Red-DiscordBot/pull/2465
|
||||||
|
.. _#2467: https://github.com/Cog-Creators/Red-DiscordBot/pull/2467
|
||||||
|
.. _#2469: https://github.com/Cog-Creators/Red-DiscordBot/pull/2469
|
||||||
|
.. _#2470: https://github.com/Cog-Creators/Red-DiscordBot/pull/2470
|
||||||
|
.. _#2472: https://github.com/Cog-Creators/Red-DiscordBot/pull/2472
|
||||||
|
.. _#2473: https://github.com/Cog-Creators/Red-DiscordBot/pull/2473
|
||||||
|
.. _#2476: https://github.com/Cog-Creators/Red-DiscordBot/pull/2476
|
||||||
|
.. _#2481: https://github.com/Cog-Creators/Red-DiscordBot/pull/2481
|
||||||
|
.. _#2482: https://github.com/Cog-Creators/Red-DiscordBot/pull/2482
|
||||||
|
.. _#2496: https://github.com/Cog-Creators/Red-DiscordBot/pull/2496
|
||||||
|
.. _#2507: https://github.com/Cog-Creators/Red-DiscordBot/pull/2507
|
||||||
|
.. _#2509: https://github.com/Cog-Creators/Red-DiscordBot/pull/2509
|
||||||
|
.. _#2513: https://github.com/Cog-Creators/Red-DiscordBot/pull/2513
|
||||||
|
.. _#2521: https://github.com/Cog-Creators/Red-DiscordBot/pull/2521
|
||||||
|
.. _#2523: https://github.com/Cog-Creators/Red-DiscordBot/pull/2523
|
||||||
|
.. _#2525: https://github.com/Cog-Creators/Red-DiscordBot/pull/2525
|
||||||
|
.. _#2531: https://github.com/Cog-Creators/Red-DiscordBot/pull/2531
|
||||||
|
.. _#2533: https://github.com/Cog-Creators/Red-DiscordBot/pull/2533
|
||||||
|
.. _#2536: https://github.com/Cog-Creators/Red-DiscordBot/pull/2536
|
||||||
|
.. _#2540: https://github.com/Cog-Creators/Red-DiscordBot/pull/2540
|
||||||
|
.. _#2545: https://github.com/Cog-Creators/Red-DiscordBot/pull/2545
|
||||||
|
.. _#2550: https://github.com/Cog-Creators/Red-DiscordBot/pull/2550
|
||||||
|
.. _#2553: https://github.com/Cog-Creators/Red-DiscordBot/pull/2553
|
||||||
|
.. _#2554: https://github.com/Cog-Creators/Red-DiscordBot/pull/2554
|
||||||
|
.. _#2556: https://github.com/Cog-Creators/Red-DiscordBot/pull/2556
|
||||||
|
.. _#2557: https://github.com/Cog-Creators/Red-DiscordBot/pull/2557
|
||||||
|
.. _#2565: https://github.com/Cog-Creators/Red-DiscordBot/pull/2565
|
||||||
|
.. _#2567: https://github.com/Cog-Creators/Red-DiscordBot/pull/2567
|
||||||
|
.. _#2576: https://github.com/Cog-Creators/Red-DiscordBot/pull/2576
|
||||||
|
.. _#2579: https://github.com/Cog-Creators/Red-DiscordBot/pull/2579
|
||||||
|
.. _#2586: https://github.com/Cog-Creators/Red-DiscordBot/pull/2586
|
||||||
|
.. _#2587: https://github.com/Cog-Creators/Red-DiscordBot/pull/2587
|
||||||
|
.. _#2588: https://github.com/Cog-Creators/Red-DiscordBot/pull/2588
|
||||||
|
.. _#2590: https://github.com/Cog-Creators/Red-DiscordBot/pull/2590
|
||||||
|
.. _#2591: https://github.com/Cog-Creators/Red-DiscordBot/pull/2591
|
||||||
|
.. _#2592: https://github.com/Cog-Creators/Red-DiscordBot/pull/2592
|
||||||
|
.. _#2595: https://github.com/Cog-Creators/Red-DiscordBot/pull/2595
|
||||||
|
.. _#2597: https://github.com/Cog-Creators/Red-DiscordBot/pull/2597
|
||||||
|
.. _#2600: https://github.com/Cog-Creators/Red-DiscordBot/pull/2600
|
||||||
|
.. _#2602: https://github.com/Cog-Creators/Red-DiscordBot/pull/2602
|
||||||
|
.. _#2604: https://github.com/Cog-Creators/Red-DiscordBot/pull/2604
|
||||||
|
.. _#2605: https://github.com/Cog-Creators/Red-DiscordBot/pull/2605
|
||||||
|
.. _#2606: https://github.com/Cog-Creators/Red-DiscordBot/pull/2606
|
||||||
|
.. _#2620: https://github.com/Cog-Creators/Red-DiscordBot/pull/2620
|
||||||
|
.. _#2628: https://github.com/Cog-Creators/Red-DiscordBot/pull/2628
|
||||||
|
.. _#2639: https://github.com/Cog-Creators/Red-DiscordBot/pull/2639
|
||||||
|
.. _#2642: https://github.com/Cog-Creators/Red-DiscordBot/pull/2642
|
||||||
|
.. _#2652: https://github.com/Cog-Creators/Red-DiscordBot/pull/2652
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
.. Importing data from a V2 install
|
|
||||||
|
|
||||||
================================
|
|
||||||
Importing data from a V2 install
|
|
||||||
================================
|
|
||||||
|
|
||||||
----------------
|
|
||||||
What you'll need
|
|
||||||
----------------
|
|
||||||
|
|
||||||
1. A Running V3 bot
|
|
||||||
2. The path where your V2 bot is installed
|
|
||||||
|
|
||||||
--------------
|
|
||||||
Importing data
|
|
||||||
--------------
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
Unless otherwise specified, the V2 data will take priority over V3 data for the same entires
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
For the purposes of this guide, your prefix will be denoted as
|
|
||||||
[p]
|
|
||||||
|
|
||||||
You should swap whatever you made your prefix in for this.
|
|
||||||
All of the below are commands to be entered in discord where the bot can
|
|
||||||
see them.
|
|
||||||
|
|
||||||
The dataconverter cog is not loaded by default. To start, load it with
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
[p]load dataconverter
|
|
||||||
|
|
||||||
Next, you'll need to give it the path where your V2 install is.
|
|
||||||
|
|
||||||
On linux and OSX, it may look something like:
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
/home/username/Red-DiscordBot/
|
|
||||||
|
|
||||||
On Windows it will look something like:
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
C:\Users\yourusername\Red-DiscordBot
|
|
||||||
|
|
||||||
Once you have that path, give it to the bot with the following command
|
|
||||||
(make sure to swap your own path in)
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
[p]convertdata /home/username/Red-DiscordBot/
|
|
||||||
|
|
||||||
|
|
||||||
From here, if the path is correct, you will be prompted with an interactive menu asking you
|
|
||||||
what data you would like to import
|
|
||||||
|
|
||||||
You can select an entry by number, or quit with any of 'quit', 'exit', 'q', '-1', or 'cancel'
|
|
||||||
@@ -72,15 +72,15 @@ Locking the ``[p]play`` command to approved server(s) as a bot owner:
|
|||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
[p]permissions setglobaldefault play deny
|
[p]permissions setdefaultglobalrule deny play
|
||||||
[p]permissions addglobalrule allow play [server ID or name]
|
[p]permissions addglobalrule allow play [server ID or name]
|
||||||
|
|
||||||
Locking the ``[p]play`` command to specific voice channel(s) as a serverowner or admin:
|
Locking the ``[p]play`` command to specific voice channel(s) as a serverowner or admin:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
[p]permissions setserverdefault deny play
|
[p]permissions setdefaultserverrule deny play
|
||||||
[p]permissions setserverdefault deny "playlist start"
|
[p]permissions setdefaultserverrule deny "playlist start"
|
||||||
[p]permissions addserverrule allow play [voice channel ID or name]
|
[p]permissions addserverrule allow play [voice channel ID or name]
|
||||||
[p]permissions addserverrule allow "playlist start" [voice channel ID or name]
|
[p]permissions addserverrule allow "playlist start" [voice channel ID or name]
|
||||||
|
|
||||||
|
|||||||
15
docs/conf.py
15
docs/conf.py
@@ -40,7 +40,7 @@ extensions = [
|
|||||||
"sphinx.ext.viewcode",
|
"sphinx.ext.viewcode",
|
||||||
"sphinx.ext.napoleon",
|
"sphinx.ext.napoleon",
|
||||||
"sphinx.ext.doctest",
|
"sphinx.ext.doctest",
|
||||||
"sphinxcontrib.asyncio",
|
"sphinxcontrib_trio",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
@@ -100,6 +100,9 @@ default_role = "any"
|
|||||||
#
|
#
|
||||||
html_theme = "sphinx_rtd_theme"
|
html_theme = "sphinx_rtd_theme"
|
||||||
|
|
||||||
|
# This will be needed until sphinx_rtd_theme supports the html5 writer
|
||||||
|
html4_writer = True
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
# 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
|
# further. For a list of options available for each theme, see the
|
||||||
# documentation.
|
# documentation.
|
||||||
@@ -195,15 +198,15 @@ texinfo_documents = [
|
|||||||
|
|
||||||
# A list of regular expressions that match URIs that should not be
|
# A list of regular expressions that match URIs that should not be
|
||||||
# checked when doing a linkcheck build.
|
# checked when doing a linkcheck build.
|
||||||
linkcheck_ignore = [r"https://java.com*"]
|
linkcheck_ignore = [r"https://java.com*", r"https://chocolatey.org*"]
|
||||||
|
|
||||||
|
|
||||||
# -- Options for extensions -----------------------------------------------
|
# -- Options for extensions -----------------------------------------------
|
||||||
|
|
||||||
# Intersphinx
|
# Intersphinx
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
"python": ("https://docs.python.org/3.6", None),
|
"python": ("https://docs.python.org/3", None),
|
||||||
"dpy": ("https://discordpy.readthedocs.io/en/rewrite/", None),
|
"dpy": ("https://discordpy.readthedocs.io/en/v1.0.1/", None),
|
||||||
"motor": ("https://motor.readthedocs.io/en/stable/", None),
|
"motor": ("https://motor.readthedocs.io/en/stable/", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,3 +214,7 @@ intersphinx_mapping = {
|
|||||||
# If this string is non-empty, all blocks with ``>>>`` in them will be
|
# If this string is non-empty, all blocks with ``>>>`` in them will be
|
||||||
# tested, not just the ones explicitly marked with ``.. doctest::``
|
# tested, not just the ones explicitly marked with ``.. doctest::``
|
||||||
doctest_test_doctest_blocks = ""
|
doctest_test_doctest_blocks = ""
|
||||||
|
|
||||||
|
# Autodoc options
|
||||||
|
autodoc_default_flags = ["show-inheritance"]
|
||||||
|
autodoc_typehints = "none"
|
||||||
|
|||||||
48
docs/framework_apikeys.rst
Normal file
48
docs/framework_apikeys.rst
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
.. V3 Shared API Key Reference
|
||||||
|
|
||||||
|
===============
|
||||||
|
Shared API Keys
|
||||||
|
===============
|
||||||
|
|
||||||
|
Red has a central API key storage utilising the core bots config. This allows cog creators to add a single location to store API keys for their cogs which may be shared between other cogs.
|
||||||
|
|
||||||
|
There needs to be some consistency between cog creators when using shared API keys between cogs. To help make this easier service should be all **lowercase** and the key names should match the naming convetion of the API being accessed.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
Twitch has a client ID and client secret so a user should be asked to input
|
||||||
|
|
||||||
|
``[p]set api twitch client_id,1234ksdjf client_secret,1234aldlfkd``
|
||||||
|
|
||||||
|
and when accessed in the code it should be done by
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
await self.bot.db.api_tokens.get_raw("twitch", default={"client_id": None, "client_secret": None})
|
||||||
|
|
||||||
|
Each service has its own dict of key, value pairs for each required key type. If there's only one key required then a name for the key is still required for storing and accessing.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
``[p]set api youtube api_key,1234ksdjf``
|
||||||
|
|
||||||
|
and when accessed in the code it should be done by
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
await self.bot.db.api_tokens.get_raw("youtube", default={"api_key": None})
|
||||||
|
|
||||||
|
|
||||||
|
***********
|
||||||
|
Basic Usage
|
||||||
|
***********
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class MyCog:
|
||||||
|
@commands.command()
|
||||||
|
async def youtube(self, ctx, user: str):
|
||||||
|
apikey = await self.bot.db.api_tokens.get_raw("youtube", default={"api_key": None})
|
||||||
|
if apikey["api_key"] is None:
|
||||||
|
return await ctx.send("The YouTube API key has not been set.")
|
||||||
|
# Use the API key to access content as you normally would
|
||||||
@@ -16,15 +16,16 @@ Basic Usage
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from redbot.core import bank
|
from redbot.core import bank, commands
|
||||||
|
import discord
|
||||||
|
|
||||||
class MyCog:
|
class MyCog(commands.Cog):
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def balance(self, ctx, user: discord.Member=None):
|
async def balance(self, ctx, user: discord.Member = None):
|
||||||
if user is None:
|
if user is None:
|
||||||
user = ctx.author
|
user = ctx.author
|
||||||
bal = bank.get_balance(user)
|
bal = await bank.get_balance(user)
|
||||||
currency = bank.get_currency_name(ctx.guild)
|
currency = await bank.get_currency_name(ctx.guild)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"{}'s balance is {} {}".format(
|
"{}'s balance is {} {}".format(
|
||||||
user.display_name, bal, currency
|
user.display_name, bal, currency
|
||||||
@@ -40,3 +41,7 @@ Bank
|
|||||||
|
|
||||||
.. automodule:: redbot.core.bank
|
.. automodule:: redbot.core.bank
|
||||||
:members:
|
:members:
|
||||||
|
:exclude-members: cost
|
||||||
|
|
||||||
|
.. autofunction:: cost
|
||||||
|
:decorator:
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ Keys common to both repo and cog info.json (case sensitive)
|
|||||||
Keys specific to the cog info.json (case sensitive)
|
Keys specific to the cog info.json (case sensitive)
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
- ``bot_version`` (list of integer) - Min version number of Red in the format ``(MAJOR, MINOR, PATCH)``
|
- ``min_bot_version`` (string) - Min version number of Red in the format ``MAJOR.MINOR.MICRO``
|
||||||
|
|
||||||
|
- ``max_bot_version`` (string) - Max version number of Red in the format ``MAJOR.MINOR.MICRO``,
|
||||||
|
if ``min_bot_version`` is newer than ``max_bot_version``, ``max_bot_version`` will be ignored
|
||||||
|
|
||||||
- ``hidden`` (bool) - Determines if a cog is visible in the cog list for a repo.
|
- ``hidden`` (bool) - Determines if a cog is visible in the cog list for a repo.
|
||||||
|
|
||||||
|
|||||||
@@ -16,17 +16,17 @@ Basic Usage
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from redbot.core import modlog
|
from redbot.core import commands, modlog
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
class MyCog:
|
class MyCog(commands.Cog):
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.admin_or_permissions(ban_members=True)
|
@checks.admin_or_permissions(ban_members=True)
|
||||||
async def ban(self, ctx, user: discord.Member, reason: str=None):
|
async def ban(self, ctx, user: discord.Member, reason: str = None):
|
||||||
await ctx.guild.ban(user)
|
await ctx.guild.ban(user)
|
||||||
case = modlog.create_case(
|
case = await modlog.create_case(
|
||||||
ctx.guild, ctx.message.created_at, "ban", user,
|
ctx.bot, ctx.guild, ctx.message.created_at, action="ban",
|
||||||
ctx.author, reason, until=None, channel=None
|
user=user, moderator=ctx.author, reason=reason
|
||||||
)
|
)
|
||||||
await ctx.send("Done. It was about time.")
|
await ctx.send("Done. It was about time.")
|
||||||
|
|
||||||
@@ -35,50 +35,65 @@ Basic Usage
|
|||||||
Registering Case types
|
Registering Case types
|
||||||
**********************
|
**********************
|
||||||
|
|
||||||
To register a single case type:
|
To register case types, use an asynchronous ``initialize()`` method and call
|
||||||
|
it from your setup function:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from redbot.core import modlog
|
# mycog/mycog.py
|
||||||
|
from redbot.core import modlog, commands
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
class MyCog:
|
class MyCog(commands.Cog):
|
||||||
def __init__(self, bot):
|
|
||||||
|
async def initialize(self):
|
||||||
|
await self.register_casetypes()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def register_casetypes():
|
||||||
|
# Registering a single casetype
|
||||||
ban_case = {
|
ban_case = {
|
||||||
"name": "ban",
|
"name": "ban",
|
||||||
"default_setting": True,
|
"default_setting": True,
|
||||||
"image": ":hammer:",
|
"image": "\N{HAMMER}",
|
||||||
"case_str": "Ban",
|
"case_str": "Ban",
|
||||||
"audit_type": "ban"
|
# audit_type should be omitted if the action doesn't show
|
||||||
|
# up in the audit log.
|
||||||
|
"audit_type": "ban",
|
||||||
}
|
}
|
||||||
modlog.register_casetype(**ban_case)
|
try:
|
||||||
|
await modlog.register_casetype(**ban_case)
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
To register multiple case types:
|
# Registering multiple casetypes
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from redbot.core import modlog
|
|
||||||
import discord
|
|
||||||
|
|
||||||
class MyCog:
|
|
||||||
def __init__(self, bot):
|
|
||||||
new_types = [
|
new_types = [
|
||||||
{
|
{
|
||||||
"name": "ban",
|
"name": "hackban",
|
||||||
"default_setting": True,
|
"default_setting": True,
|
||||||
"image": ":hammer:",
|
"image": "\N{BUST IN SILHOUETTE}\N{HAMMER}",
|
||||||
"case_str": "Ban",
|
"case_str": "Hackban",
|
||||||
"audit_type": "ban"
|
"audit_type": "ban",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "kick",
|
"name": "kick",
|
||||||
"default_setting": True,
|
"default_setting": True,
|
||||||
"image": ":boot:",
|
"image": "\N{WOMANS BOOTS}",
|
||||||
"case_str": "Kick",
|
"case_str": "Kick",
|
||||||
"audit_type": "kick"
|
"audit_type": "kick"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
modlog.register_casetypes(new_types)
|
await modlog.register_casetypes(new_types)
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# mycog/__init__.py
|
||||||
|
from .mycog import MyCog
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
cog = MyCog()
|
||||||
|
await cog.initialize()
|
||||||
|
bot.add_cog(cog)
|
||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
Image should be the emoji you want to represent your case type with.
|
Image should be the emoji you want to represent your case type with.
|
||||||
|
|||||||
@@ -40,12 +40,6 @@ Mod Helpers
|
|||||||
.. automodule:: redbot.core.utils.mod
|
.. automodule:: redbot.core.utils.mod
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
V2 Data Conversion
|
|
||||||
==================
|
|
||||||
|
|
||||||
.. automodule:: redbot.core.utils.data_converter
|
|
||||||
:members: DataConverter
|
|
||||||
|
|
||||||
Tunnel
|
Tunnel
|
||||||
======
|
======
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ you in the process.
|
|||||||
Getting started
|
Getting started
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
To start off, be sure that you have installed Python 3.6.2 or higher (3.6.6 or higher on Windows).
|
To start off, be sure that you have installed Python 3.7.
|
||||||
Open a terminal or command prompt and type :code:`pip install -U git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=redbot[test]`
|
Open a terminal or command prompt and type :code:`pip install -U git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=redbot[test]`
|
||||||
(note that if you get an error with this, try again but put :code:`python -m` in front of the command
|
(note that if you get an error with this, try again but put :code:`python -m` in front of the command
|
||||||
This will install the latest version of V3.
|
This will install the latest version of V3.
|
||||||
@@ -46,7 +46,7 @@ In that file, place the following code:
|
|||||||
|
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
|
|
||||||
class Mycog:
|
class Mycog(commands.Cog):
|
||||||
"""My custom cog"""
|
"""My custom cog"""
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
.. Converting Data from a V2 cog
|
|
||||||
|
|
||||||
.. role:: python(code)
|
|
||||||
:language: python
|
|
||||||
|
|
||||||
============================
|
|
||||||
Importing Data From a V2 Cog
|
|
||||||
============================
|
|
||||||
|
|
||||||
This guide serves as a tutorial on using the DataConverter class
|
|
||||||
to import settings from a V2 cog.
|
|
||||||
|
|
||||||
------------------
|
|
||||||
Things you'll need
|
|
||||||
------------------
|
|
||||||
|
|
||||||
1. The path where each file holding related settings in v2 is
|
|
||||||
2. A conversion function to take the data and transform it to conform to Config
|
|
||||||
|
|
||||||
-----------------------
|
|
||||||
Getting your file paths
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
You should probably not try to find the files manually.
|
|
||||||
Asking the user for the base install path and using a relative path to where the
|
|
||||||
data should be, then testing that the file exists there is safer. This is especially
|
|
||||||
True if your cog has multiple settings files
|
|
||||||
|
|
||||||
Example
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from discord.ext import commands
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
@commands.command(name="filefinder")
|
|
||||||
async def file_finding_command(self, ctx, filepath):
|
|
||||||
"""
|
|
||||||
this finds a file based on a user provided input and a known relative path
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_path = Path(filepath)
|
|
||||||
fp = base_path / 'data' / 'mycog' / 'settings.json'
|
|
||||||
if not fp.is_file():
|
|
||||||
pass
|
|
||||||
# fail, prompting user
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
# do something with the file
|
|
||||||
|
|
||||||
---------------
|
|
||||||
Converting data
|
|
||||||
---------------
|
|
||||||
|
|
||||||
Once you've gotten your v2 settings file, you'll want to be able to import it
|
|
||||||
There are a couple options available depending on how you would like to convert
|
|
||||||
the data.
|
|
||||||
|
|
||||||
The first one takes a data path, and a conversion function and does the rest for you.
|
|
||||||
This is great for simple data that just needs to quickly be imported without much
|
|
||||||
modification.
|
|
||||||
|
|
||||||
|
|
||||||
Here's an example of that in use:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from redbot.core.utils.data_converter import DataConverter as dc
|
|
||||||
from redbot.core.config import Config
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
async def import_v2(self, file_path: Path):
|
|
||||||
"""
|
|
||||||
to be called from a command limited to owner
|
|
||||||
|
|
||||||
This should be a coroutine as the convert function will
|
|
||||||
need to be awaited
|
|
||||||
"""
|
|
||||||
|
|
||||||
# First we give the converter our cog's Config instance.
|
|
||||||
converter = dc(self.config)
|
|
||||||
|
|
||||||
# next we design a way to get all of the data into Config's internal
|
|
||||||
# format. This should be a generator, but you can also return a single
|
|
||||||
# list with identical results outside of memory usage
|
|
||||||
def conversion_spec(v2data):
|
|
||||||
for guild_id in v2.data.keys():
|
|
||||||
yield {(Config.GUILD, guild_id): {('blacklisted',): True}}
|
|
||||||
# This is yielding a dictionary that is designed for config's set_raw.
|
|
||||||
# The keys should be a tuple of Config scopes + the needed Identifiers. The
|
|
||||||
# values should be another dictionary whose keys are tuples representing
|
|
||||||
# config settings, the value should be the value to set for that.
|
|
||||||
|
|
||||||
# Then we pass the file and the conversion function
|
|
||||||
await converter.convert(file_path, conversion_spec)
|
|
||||||
# From here, our data should be imported
|
|
||||||
|
|
||||||
|
|
||||||
You can also choose to convert all of your data and pass it as a single dict
|
|
||||||
This can be useful if you want finer control over the dataconversion or want to
|
|
||||||
preserve any data from v3 that may share the same entry and set it aside to prompt
|
|
||||||
a user
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from redbot.core.utils.data_converter import DataConverter as dc
|
|
||||||
from redbot.core.config import Config
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
await dc(config_instance).dict_import(some_processed_dict)
|
|
||||||
|
|
||||||
|
|
||||||
The format of the items of the dict is the same as in the above example
|
|
||||||
|
|
||||||
|
|
||||||
-----------------------------------
|
|
||||||
Config Scopes and their Identifiers
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
This section is provided as a quick reference for the identifiers for default
|
|
||||||
scopes available in Config. This does not cover usage of custom scopes, though the
|
|
||||||
data converter is compatible with those as well.
|
|
||||||
|
|
||||||
Global::
|
|
||||||
:code:`(Config.GLOBAL,)`
|
|
||||||
Guild::
|
|
||||||
:code:`(Config.GUILD, guild_id)`
|
|
||||||
Channel::
|
|
||||||
:code:`(Config.CHANNEL, channel_id)`
|
|
||||||
User::
|
|
||||||
:code:`(Config.USER, user_id)`
|
|
||||||
Member::
|
|
||||||
:code:`(Config.MEMBER, guild_id, user_id)`
|
|
||||||
Role::
|
|
||||||
:code:`(Config.ROLE, role_id)`
|
|
||||||
|
|
||||||
|
|
||||||
-----------------------------
|
|
||||||
More information and Examples
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
For a more in depth look at how all of these commands function
|
|
||||||
You may want to take a look at how core data is being imported
|
|
||||||
|
|
||||||
:code:`redbot/cogs/dataconverter/core_specs.py`
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
Migrating Cogs to V3
|
Migrating Cogs to V3
|
||||||
====================
|
====================
|
||||||
|
|
||||||
First, be sure to read `discord.py's migration guide <http://discordpy.readthedocs.io/en/rewrite/migrating.html>`_
|
First, be sure to read `discord.py's migration guide <https://discordpy.readthedocs.io/en/v1.0.1/migrating.html>`_
|
||||||
as that covers all of the changes to discord.py that will affect the migration process
|
as that covers all of the changes to discord.py that will affect the migration process
|
||||||
|
|
||||||
----------------
|
----------------
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
install_windows
|
install_windows
|
||||||
install_linux_mac
|
install_linux_mac
|
||||||
venv_guide
|
venv_guide
|
||||||
cog_dataconverter
|
|
||||||
autostart_systemd
|
autostart_systemd
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
@@ -30,7 +29,7 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
|
|
||||||
guide_migration
|
guide_migration
|
||||||
guide_cog_creation
|
guide_cog_creation
|
||||||
guide_data_conversion
|
framework_apikeys
|
||||||
framework_bank
|
framework_bank
|
||||||
framework_bot
|
framework_bot
|
||||||
framework_checks
|
framework_checks
|
||||||
@@ -45,6 +44,11 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
framework_rpc
|
framework_rpc
|
||||||
framework_utils
|
framework_utils
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: Changelogs:
|
||||||
|
|
||||||
|
changelog_3_1_0
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ Installing the pre-requirements
|
|||||||
Please install the pre-requirements using the commands listed for your operating system.
|
Please install the pre-requirements using the commands listed for your operating system.
|
||||||
|
|
||||||
The pre-requirements are:
|
The pre-requirements are:
|
||||||
- Python 3.6.2 or greater
|
- Python 3.7.0 or greater
|
||||||
- pip 9.0 or greater
|
- pip 9.0 or greater
|
||||||
- git
|
- git
|
||||||
- Java Runtime Environment 8 or later (for audio support)
|
- Java Runtime Environment 8 or later (for audio support)
|
||||||
|
|
||||||
|
.. _install-arch:
|
||||||
|
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
Arch Linux
|
Arch Linux
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
@@ -29,15 +31,25 @@ Arch Linux
|
|||||||
|
|
||||||
sudo pacman -Syu python-pip git base-devel jre8-openjdk
|
sudo pacman -Syu python-pip git base-devel jre8-openjdk
|
||||||
|
|
||||||
~~~~~~~~
|
.. _install-centos:
|
||||||
CentOS 7
|
.. _install-fedora:
|
||||||
~~~~~~~~
|
.. _install-rhel:
|
||||||
|
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
CentOS 7, Fedora, and RHEL
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
yum -y groupinstall development
|
yum -y groupinstall development
|
||||||
yum -y install https://centos7.iuscommunity.org/ius-release.rpm
|
yum -y install https://centos7.iuscommunity.org/ius-release.rpm
|
||||||
yum -y install yum-utils wget which python36u python36u-pip python36u-devel openssl-devel libffi-devel git java-1.8.0-openjdk
|
sudo yum install zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel \
|
||||||
|
openssl-devel xz xz-devel libffi-devel git2u java-1.8.0-openjdk
|
||||||
|
|
||||||
|
Complete the rest of the installation by `installing Python 3.7 with pyenv <install-python-pyenv>`.
|
||||||
|
|
||||||
|
.. _install-debian:
|
||||||
|
.. _install-raspbian:
|
||||||
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Debian and Raspbian Stretch
|
Debian and Raspbian Stretch
|
||||||
@@ -53,27 +65,13 @@ Debian/Raspbian Stretch. This guide will tell you how. First, run the following
|
|||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
sudo apt install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev git unzip default-jre
|
sudo apt install -y make build-essential libssl-dev zlib1g-dev libbz2-dev \
|
||||||
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
|
libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev \
|
||||||
|
xz-utils tk-dev libffi-dev liblzma-dev python3-openssl git unzip default-jre
|
||||||
|
|
||||||
After that last command, you may see a warning about 'pyenv' not being in the load path. Follow the
|
Complete the rest of the installation by `installing Python 3.7 with pyenv <install-python-pyenv>`.
|
||||||
instructions given to fix that, then close and reopen your shell.
|
|
||||||
|
|
||||||
Then run the following command:
|
.. _install-mac:
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
CONFIGURE_OPTS=--enable-optimizations pyenv install 3.7.0 -v
|
|
||||||
|
|
||||||
This may take a long time to complete.
|
|
||||||
|
|
||||||
After that is finished, run:
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
pyenv global 3.7.0
|
|
||||||
|
|
||||||
Pyenv is now installed and your system should be configured to run Python 3.7.
|
|
||||||
|
|
||||||
~~~
|
~~~
|
||||||
Mac
|
Mac
|
||||||
@@ -91,24 +89,33 @@ one-by-one:
|
|||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
brew install python3 --with-brewed-openssl
|
brew install python --with-brewed-openssl
|
||||||
brew install git
|
brew install git
|
||||||
brew tap caskroom/versions
|
brew tap caskroom/versions
|
||||||
brew cask install java8
|
brew cask install homebrew/cask-versions/adoptopenjdk8
|
||||||
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
It's possible you will have network issues. If so, go in your Applications folder, inside it, go in the Python 3.7 folder then double click ``Install certificates.command``
|
||||||
Ubuntu 18.04 Bionic Beaver
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
.. _install-ubuntu:
|
||||||
|
.. _install-ubuntu-bionic:
|
||||||
|
.. _install-ubuntu-cosmic:
|
||||||
|
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Ubuntu 18.04 Bionic Beaver and 18.10 Cosmic Cuttlefish
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
sudo apt install python3.6-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
|
sudo apt install python3.7 python3.7-dev python3.7-venv python3-pip build-essential \
|
||||||
|
libssl-dev libffi-dev git unzip default-jre -y
|
||||||
|
|
||||||
|
.. _install-ubuntu-xenial:
|
||||||
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Ubuntu 16.04 Xenial Xerus
|
Ubuntu 16.04 Xenial Xerus
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
We recommend adding the ``deadsnakes`` apt repository to install Python 3.6.2 or greater:
|
We recommend adding the ``deadsnakes`` apt repository to install Python 3.7 or greater:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
@@ -120,9 +127,46 @@ Now, install python, pip, git and java with the following commands:
|
|||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
sudo apt install python3.6-dev build-essential libssl-dev libffi-dev git unzip default-jre wget -y
|
sudo apt install python3.7 python3.7-dev build-essential libssl-dev libffi-dev git \
|
||||||
wget https://bootstrap.pypa.io/get-pip.py
|
unzip default-jre curl -y
|
||||||
sudo python3.6 get-pip.py
|
curl https://bootstrap.pypa.io/get-pip.py | sudo python3.7
|
||||||
|
|
||||||
|
.. _install-python-pyenv:
|
||||||
|
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Installing Python with pyenv
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
On distributions where Python 3.7 needs to be compiled from source, we recommend the use of pyenv.
|
||||||
|
This simplifies the compilation process and has the added bonus of simplifying setting up Red in a
|
||||||
|
virtual environment.
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
|
||||||
|
|
||||||
|
After this command, you may see a warning about 'pyenv' not being in the load path. Follow the
|
||||||
|
instructions given to fix that, then close and reopen your shell.
|
||||||
|
|
||||||
|
Then run the following command:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
CONFIGURE_OPTS=--enable-optimizations pyenv install 3.7.2 -v
|
||||||
|
|
||||||
|
This may take a long time to complete, depending on your hardware. For some machines (such as
|
||||||
|
Raspberry Pis and micro-tier VPSes), it may take over an hour; in this case, you may wish to remove
|
||||||
|
the ``CONFIGURE_OPTS=--enable-optimizations`` part from the front of the command, which will
|
||||||
|
drastically reduce the install time. However, be aware that this will make Python run about 10%
|
||||||
|
slower.
|
||||||
|
|
||||||
|
After that is finished, run:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
pyenv global 3.7.2
|
||||||
|
|
||||||
|
Pyenv is now installed and your system should be configured to run Python 3.7.
|
||||||
|
|
||||||
------------------------------
|
------------------------------
|
||||||
Creating a Virtual Environment
|
Creating a Virtual Environment
|
||||||
@@ -142,25 +186,19 @@ Choose one of the following commands to install Red.
|
|||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
If you're not inside an activated virtual environment, include the ``--user`` flag with all
|
If you're not inside an activated virtual environment, include the ``--user`` flag with all
|
||||||
``pip3`` commands.
|
``python3.7 -m pip`` commands.
|
||||||
|
|
||||||
To install without audio support:
|
To install without MongoDB support:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
pip3 install -U Red-DiscordBot
|
python3.7 -m pip install -U Red-DiscordBot
|
||||||
|
|
||||||
Or, to install with audio support:
|
Or, to install with MongoDB support:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
pip3 install -U Red-DiscordBot[voice]
|
python3.7 -m pip install -U Red-DiscordBot[mongo]
|
||||||
|
|
||||||
Or, install with audio and MongoDB support:
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
pip3 install -U Red-DiscordBot[voice,mongo]
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@@ -192,6 +230,9 @@ Once done setting up the instance, run the following command to run Red:
|
|||||||
redbot <your instance name>
|
redbot <your instance name>
|
||||||
|
|
||||||
It will walk through the initial setup, asking for your token and a prefix.
|
It will walk through the initial setup, asking for your token and a prefix.
|
||||||
|
You can find out how to obtain a token with
|
||||||
|
`this guide <https://discordpy.readthedocs.io/en/v1.0.1/discord.html#creating-a-bot-account>`_,
|
||||||
|
section "Creating a Bot Account".
|
||||||
|
|
||||||
You may also run Red via the launcher, which allows you to restart the bot
|
You may also run Red via the launcher, which allows you to restart the bot
|
||||||
from discord, and enable auto-restart. You may also update the bot from the
|
from discord, and enable auto-restart. You may also update the bot from the
|
||||||
|
|||||||
@@ -8,18 +8,44 @@ Installing Red on Windows
|
|||||||
Needed Software
|
Needed Software
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
* `Python <https://www.python.org/downloads/>`_ - Red needs Python 3.6.6 or greater on Windows
|
The following software dependencies can all be installed quickly and easily through powershell,
|
||||||
|
using a trusted package manager for windows called `Chocolatey <https://chocolatey.org>`_
|
||||||
|
|
||||||
|
We also provide instructions for manually installing all of the dependencies.
|
||||||
|
|
||||||
|
******************************************
|
||||||
|
Installing using powershell and chocolatey
|
||||||
|
******************************************
|
||||||
|
|
||||||
|
To install via powershell, search "powershell" in the windows start menu,
|
||||||
|
right-click on it and then click "Run as administrator"
|
||||||
|
|
||||||
|
Then run each of the following commands:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||||
|
iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
|
||||||
|
choco install git --params "/GitOnlyOnPath /WindowsTerminal" -y
|
||||||
|
choco install jre8 python -y; exit
|
||||||
|
|
||||||
|
|
||||||
|
********************************
|
||||||
|
Manually installing dependencies
|
||||||
|
********************************
|
||||||
|
|
||||||
|
* `Python <https://www.python.org/downloads/>`_ - Red needs Python 3.7.0 or greater
|
||||||
|
|
||||||
.. note:: Please make sure that the box to add Python to PATH is CHECKED, otherwise
|
.. note:: Please make sure that the box to add Python to PATH is CHECKED, otherwise
|
||||||
you may run into issues when trying to run Red
|
you may run into issues when trying to run Red.
|
||||||
|
|
||||||
* `Git <https://git-scm.com/download/win>`_
|
* `Git <https://git-scm.com/download/win>`_
|
||||||
|
|
||||||
.. attention:: Please choose the option to "Run Git from the Windows Command Prompt" in Git's setup
|
.. attention:: Please choose the option to "Run Git from the Windows Command Prompt" in Git's setup.
|
||||||
|
|
||||||
* `Java <https://java.com/en/download/manual.jsp>`_ - needed for Audio
|
* `Java <https://java.com/en/download/manual.jsp>`_ - needed for Audio
|
||||||
|
|
||||||
.. attention:: Please choose the "Windows Online" installer
|
.. attention:: Please choose the "Windows Online" installer.
|
||||||
|
|
||||||
.. _installing-red-windows:
|
.. _installing-red-windows:
|
||||||
|
|
||||||
@@ -27,6 +53,9 @@ Needed Software
|
|||||||
Installing Red
|
Installing Red
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
.. attention:: You may need to restart your computer after installing dependencies
|
||||||
|
for the PATH changes to take effect.
|
||||||
|
|
||||||
1. Open a command prompt (open Start, search for "command prompt", then click it)
|
1. Open a command prompt (open Start, search for "command prompt", then click it)
|
||||||
2. Create and activate a virtual environment (strongly recommended), see the section `using-venv`
|
2. Create and activate a virtual environment (strongly recommended), see the section `using-venv`
|
||||||
3. Run **one** of the following commands, depending on what extras you want installed
|
3. Run **one** of the following commands, depending on what extras you want installed
|
||||||
@@ -36,23 +65,17 @@ Installing Red
|
|||||||
If you're not inside an activated virtual environment, include the ``--user`` flag with all
|
If you're not inside an activated virtual environment, include the ``--user`` flag with all
|
||||||
``pip`` commands.
|
``pip`` commands.
|
||||||
|
|
||||||
* No audio:
|
* No MongoDB support:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
python -m pip install -U Red-DiscordBot
|
python -m pip install -U Red-DiscordBot
|
||||||
|
|
||||||
* With audio:
|
* With MongoDB support:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
python -m pip install -U Red-DiscordBot[voice]
|
python -m pip install -U Red-DiscordBot[mongo]
|
||||||
|
|
||||||
* With audio and MongoDB support:
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
python -m pip install -U Red-DiscordBot[voice,mongo]
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@@ -84,6 +107,9 @@ Once done setting up the instance, run the following command to run Red:
|
|||||||
redbot <your instance name>
|
redbot <your instance name>
|
||||||
|
|
||||||
It will walk through the initial setup, asking for your token and a prefix.
|
It will walk through the initial setup, asking for your token and a prefix.
|
||||||
|
You can find out how to obtain a token with
|
||||||
|
`this guide <https://discordpy.readthedocs.io/en/v1.0.1/discord.html#creating-a-bot-account>`_,
|
||||||
|
section "Creating a Bot Account".
|
||||||
|
|
||||||
You may also run Red via the launcher, which allows you to restart the bot
|
You may also run Red via the launcher, which allows you to restart the bot
|
||||||
from discord, and enable auto-restart. You may also update the bot from the
|
from discord, and enable auto-restart. You may also update the bot from the
|
||||||
|
|||||||
2
docs/requirements.txt
Normal file
2
docs/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# We still need this because RTD is special
|
||||||
|
setuptools==40.8.0
|
||||||
@@ -24,7 +24,7 @@ to keep it in a location which is easy to type out the path to. From now, we'll
|
|||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Create your virtual environment with the following command::
|
Create your virtual environment with the following command::
|
||||||
|
|
||||||
python3 -m venv path/to/venv/
|
python3.7 -m venv path/to/venv/
|
||||||
|
|
||||||
And activate it with the following command::
|
And activate it with the following command::
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ Using ``pyenv virtualenv``
|
|||||||
Using ``pyenv virtualenv`` saves you the headache of remembering where you installed your virtual
|
Using ``pyenv virtualenv`` saves you the headache of remembering where you installed your virtual
|
||||||
environments. If you haven't already, install pyenv with `pyenv-installer`_.
|
environments. If you haven't already, install pyenv with `pyenv-installer`_.
|
||||||
|
|
||||||
First, ensure your pyenv interpreter is set to python 3.6.2 or greater with the following command::
|
First, ensure your pyenv interpreter is set to python 3.7.0 or greater with the following command::
|
||||||
|
|
||||||
pyenv version
|
pyenv version
|
||||||
|
|
||||||
|
|||||||
27
make.bat
27
make.bat
@@ -14,23 +14,21 @@ for /F "tokens=* USEBACKQ" %%A in (`git ls-files "*.py"`) do (
|
|||||||
goto %1
|
goto %1
|
||||||
|
|
||||||
:reformat
|
:reformat
|
||||||
black -l 99 -N !PYFILES!
|
black -l 99 !PYFILES!
|
||||||
exit /B %ERRORLEVEL%
|
exit /B %ERRORLEVEL%
|
||||||
|
|
||||||
:stylecheck
|
:stylecheck
|
||||||
black -l 99 -N --check !PYFILES!
|
black -l 99 --check !PYFILES!
|
||||||
exit /B %ERRORLEVEL%
|
exit /B %ERRORLEVEL%
|
||||||
|
|
||||||
:update_vendor
|
:newenv
|
||||||
if [%REF%] == [] (
|
py -3.7 -m venv --clear .venv
|
||||||
set REF2="rewrite"
|
.\.venv\Scripts\python -m pip install -U pip setuptools
|
||||||
) else (
|
goto syncenv
|
||||||
set REF2=%REF%
|
|
||||||
)
|
:syncenv
|
||||||
pip install --upgrade --no-deps -t . https://github.com/Rapptz/discord.py/archive/!REF2!.tar.gz#egg=discord.py
|
.\.venv\Scripts\python -m pip install -Ur .\tools\dev-requirements.txt
|
||||||
del /S /Q "discord.py*-info"
|
exit /B %ERRORLEVEL%
|
||||||
for /F %%i in ('dir /S /B discord.py*.egg-info') do rmdir /S /Q %%i
|
|
||||||
goto reformat
|
|
||||||
|
|
||||||
:help
|
:help
|
||||||
echo Usage:
|
echo Usage:
|
||||||
@@ -39,5 +37,6 @@ echo.
|
|||||||
echo Commands:
|
echo Commands:
|
||||||
echo reformat Reformat all .py files being tracked by git.
|
echo reformat Reformat all .py files being tracked by git.
|
||||||
echo stylecheck Check which tracked .py files need reformatting.
|
echo stylecheck Check which tracked .py files need reformatting.
|
||||||
echo update_vendor Update vendored discord.py library to %%REF%%, which defaults to
|
echo newenv Create or replace this project's virtual environment.
|
||||||
echo "rewrite"
|
echo syncenv Sync this project's virtual environment to Red's latest
|
||||||
|
echo dependencies.
|
||||||
|
|||||||
@@ -1,34 +1,180 @@
|
|||||||
import sys
|
import re as _re
|
||||||
import warnings
|
import sys as _sys
|
||||||
import discord
|
import warnings as _warnings
|
||||||
import colorama
|
from math import inf as _inf
|
||||||
|
from typing import (
|
||||||
|
ClassVar as _ClassVar,
|
||||||
|
Dict as _Dict,
|
||||||
|
List as _List,
|
||||||
|
Optional as _Optional,
|
||||||
|
Pattern as _Pattern,
|
||||||
|
Tuple as _Tuple,
|
||||||
|
Union as _Union,
|
||||||
|
)
|
||||||
|
|
||||||
# Let's do all the dumb version checking in one place.
|
|
||||||
if sys.platform == "win32":
|
|
||||||
# Due to issues with ProactorEventLoop prior to 3.6.6 (bpo-26819)
|
|
||||||
MIN_PYTHON_VERSION = (3, 6, 6)
|
|
||||||
else:
|
|
||||||
MIN_PYTHON_VERSION = (3, 6, 2)
|
|
||||||
|
|
||||||
if sys.version_info < MIN_PYTHON_VERSION:
|
MIN_PYTHON_VERSION = (3, 7, 0)
|
||||||
|
|
||||||
|
__all__ = ["MIN_PYTHON_VERSION", "__version__", "version_info", "VersionInfo"]
|
||||||
|
|
||||||
|
if _sys.version_info < MIN_PYTHON_VERSION:
|
||||||
print(
|
print(
|
||||||
f"Python {'.'.join(map(str, MIN_PYTHON_VERSION))} is required to run Red, but you have "
|
f"Python {'.'.join(map(str, MIN_PYTHON_VERSION))} is required to run Red, but you have "
|
||||||
f"{sys.version}! Please update Python."
|
f"{_sys.version}! Please update Python."
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
_sys.exit(1)
|
||||||
|
|
||||||
if discord.version_info.major < 1:
|
|
||||||
print(
|
class VersionInfo:
|
||||||
"You are not running the rewritten version of discord.py.\n\n"
|
ALPHA = "alpha"
|
||||||
"In order to use Red V3 you MUST be running d.py version "
|
BETA = "beta"
|
||||||
"1.0.0 or greater."
|
RELEASE_CANDIDATE = "release candidate"
|
||||||
|
FINAL = "final"
|
||||||
|
|
||||||
|
_VERSION_STR_PATTERN: _ClassVar[_Pattern[str]] = _re.compile(
|
||||||
|
r"^"
|
||||||
|
r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<micro>0|[1-9]\d*)"
|
||||||
|
r"(?:(?P<releaselevel>a|b|rc)(?P<serial>0|[1-9]\d*))?"
|
||||||
|
r"(?:\.post(?P<post_release>0|[1-9]\d*))?"
|
||||||
|
r"(?:\.dev(?P<dev_release>0|[1-9]\d*))?"
|
||||||
|
r"$",
|
||||||
|
flags=_re.IGNORECASE,
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
_RELEASE_LEVELS: _ClassVar[_List[str]] = [ALPHA, BETA, RELEASE_CANDIDATE, FINAL]
|
||||||
|
_SHORT_RELEASE_LEVELS: _ClassVar[_Dict[str, str]] = {
|
||||||
|
"a": ALPHA,
|
||||||
|
"b": BETA,
|
||||||
|
"rc": RELEASE_CANDIDATE,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
major: int,
|
||||||
|
minor: int,
|
||||||
|
micro: int,
|
||||||
|
releaselevel: str,
|
||||||
|
serial: _Optional[int] = None,
|
||||||
|
post_release: _Optional[int] = None,
|
||||||
|
dev_release: _Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
self.major: int = major
|
||||||
|
self.minor: int = minor
|
||||||
|
self.micro: int = micro
|
||||||
|
|
||||||
|
if releaselevel not in self._RELEASE_LEVELS:
|
||||||
|
raise TypeError(f"'releaselevel' must be one of: {', '.join(self._RELEASE_LEVELS)}")
|
||||||
|
|
||||||
|
self.releaselevel: str = releaselevel
|
||||||
|
self.serial: _Optional[int] = serial
|
||||||
|
self.post_release: _Optional[int] = post_release
|
||||||
|
self.dev_release: _Optional[int] = dev_release
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_str(cls, version_str: str) -> "VersionInfo":
|
||||||
|
"""Parse a string into a VersionInfo object.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If the version info string is invalid.
|
||||||
|
|
||||||
|
"""
|
||||||
|
match = cls._VERSION_STR_PATTERN.match(version_str)
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f"Invalid version string: {version_str}")
|
||||||
|
|
||||||
|
kwargs: _Dict[str, _Union[str, int]] = {}
|
||||||
|
for key in ("major", "minor", "micro"):
|
||||||
|
kwargs[key] = int(match[key])
|
||||||
|
releaselevel = match["releaselevel"]
|
||||||
|
if releaselevel is not None:
|
||||||
|
kwargs["releaselevel"] = cls._SHORT_RELEASE_LEVELS[releaselevel]
|
||||||
|
else:
|
||||||
|
kwargs["releaselevel"] = cls.FINAL
|
||||||
|
for key in ("serial", "post_release", "dev_release"):
|
||||||
|
if match[key] is not None:
|
||||||
|
kwargs[key] = int(match[key])
|
||||||
|
return cls(**kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(
|
||||||
|
cls, data: _Union[_Dict[str, _Union[int, str]], _List[_Union[int, str]]]
|
||||||
|
) -> "VersionInfo":
|
||||||
|
if isinstance(data, _List):
|
||||||
|
# For old versions, data was stored as a list:
|
||||||
|
# [MAJOR, MINOR, MICRO, RELEASELEVEL, SERIAL]
|
||||||
|
return cls(*data)
|
||||||
|
else:
|
||||||
|
return cls(**data)
|
||||||
|
|
||||||
|
def to_json(self) -> _Dict[str, _Union[int, str]]:
|
||||||
|
return {
|
||||||
|
"major": self.major,
|
||||||
|
"minor": self.minor,
|
||||||
|
"micro": self.micro,
|
||||||
|
"releaselevel": self.releaselevel,
|
||||||
|
"serial": self.serial,
|
||||||
|
"post_release": self.post_release,
|
||||||
|
"dev_release": self.dev_release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _generate_comparison_tuples(
|
||||||
|
self, other: "VersionInfo"
|
||||||
|
) -> _List[
|
||||||
|
_Tuple[int, int, int, int, _Union[int, float], _Union[int, float], _Union[int, float]]
|
||||||
|
]:
|
||||||
|
tups: _List[
|
||||||
|
_Tuple[int, int, int, int, _Union[int, float], _Union[int, float], _Union[int, float]]
|
||||||
|
] = []
|
||||||
|
for obj in (self, other):
|
||||||
|
tups.append(
|
||||||
|
(
|
||||||
|
obj.major,
|
||||||
|
obj.minor,
|
||||||
|
obj.micro,
|
||||||
|
obj._RELEASE_LEVELS.index(obj.releaselevel),
|
||||||
|
obj.serial if obj.serial is not None else _inf,
|
||||||
|
obj.post_release if obj.post_release is not None else -_inf,
|
||||||
|
obj.dev_release if obj.dev_release is not None else _inf,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return tups
|
||||||
|
|
||||||
|
def __lt__(self, other: "VersionInfo") -> bool:
|
||||||
|
tups = self._generate_comparison_tuples(other)
|
||||||
|
return tups[0] < tups[1]
|
||||||
|
|
||||||
|
def __eq__(self, other: "VersionInfo") -> bool:
|
||||||
|
tups = self._generate_comparison_tuples(other)
|
||||||
|
return tups[0] == tups[1]
|
||||||
|
|
||||||
|
def __le__(self, other: "VersionInfo") -> bool:
|
||||||
|
tups = self._generate_comparison_tuples(other)
|
||||||
|
return tups[0] <= tups[1]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
ret = f"{self.major}.{self.minor}.{self.micro}"
|
||||||
|
if self.releaselevel != self.FINAL:
|
||||||
|
short = next(
|
||||||
|
k for k, v in self._SHORT_RELEASE_LEVELS.items() if v == self.releaselevel
|
||||||
|
)
|
||||||
|
ret += f"{short}{self.serial}"
|
||||||
|
if self.post_release is not None:
|
||||||
|
ret += f".post{self.post_release}"
|
||||||
|
if self.dev_release is not None:
|
||||||
|
ret += f".dev{self.dev_release}"
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
"VersionInfo(major={major}, minor={minor}, micro={micro}, "
|
||||||
|
"releaselevel={releaselevel}, serial={serial}, post={post_release}, "
|
||||||
|
"dev={dev_release})".format(**self.to_json())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
colorama.init()
|
__version__ = "3.1.6"
|
||||||
|
version_info = VersionInfo.from_str(__version__)
|
||||||
|
|
||||||
# Filter fuzzywuzzy slow sequence matcher warning
|
# Filter fuzzywuzzy slow sequence matcher warning
|
||||||
warnings.filterwarnings("ignore", module=r"fuzzywuzzy.*")
|
_warnings.filterwarnings("ignore", module=r"fuzzywuzzy.*")
|
||||||
# Prevent discord PyNaCl missing warning
|
|
||||||
discord.voice_client.VoiceClient.warn_nacl = False
|
|
||||||
|
|||||||
@@ -2,36 +2,43 @@
|
|||||||
|
|
||||||
# Discord Version check
|
# Discord Version check
|
||||||
|
|
||||||
import sys
|
|
||||||
import discord
|
|
||||||
from redbot.core.bot import Red, ExitCodes
|
|
||||||
from redbot.core.cog_manager import CogManagerUI
|
|
||||||
from redbot.core.data_manager import create_temp_config, load_basic_configuration, config_file
|
|
||||||
from redbot.core.json_io import JsonIO
|
|
||||||
from redbot.core.global_checks import init_global_checks
|
|
||||||
from redbot.core.events import init_events
|
|
||||||
from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry
|
|
||||||
from redbot.core.core_commands import Core
|
|
||||||
from redbot.core.dev_commands import Dev
|
|
||||||
from redbot.core import __version__
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging.handlers
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
# Let's not force this dependency, uvloop is much faster on cpython
|
import discord
|
||||||
if sys.implementation.name == "cpython":
|
|
||||||
|
# Set the event loop policies here so any subsequent `get_event_loop()`
|
||||||
|
# calls, in particular those as a result of the following imports,
|
||||||
|
# return the correct loop object.
|
||||||
|
if sys.platform == "win32":
|
||||||
|
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
||||||
|
elif sys.implementation.name == "cpython":
|
||||||
|
# Let's not force this dependency, uvloop is much faster on cpython
|
||||||
try:
|
try:
|
||||||
import uvloop
|
import uvloop
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
uvloop = None
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
|
|
||||||
if sys.platform == "win32":
|
import redbot.logging
|
||||||
asyncio.set_event_loop(asyncio.ProactorEventLoop())
|
from redbot.core.bot import Red, ExitCodes
|
||||||
|
from redbot.core.cog_manager import CogManagerUI
|
||||||
|
from redbot.core.global_checks import init_global_checks
|
||||||
|
from redbot.core.events import init_events
|
||||||
|
from redbot.core.cli import interactive_config, confirm, parse_cli_flags
|
||||||
|
from redbot.core.core_commands import Core
|
||||||
|
from redbot.core.dev_commands import Dev
|
||||||
|
from redbot.core import __version__, modlog, bank, data_manager
|
||||||
|
from signal import SIGTERM
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger("red.main")
|
||||||
|
|
||||||
#
|
#
|
||||||
# Red - Discord Bot v3
|
# Red - Discord Bot v3
|
||||||
#
|
#
|
||||||
@@ -39,50 +46,6 @@ if sys.platform == "win32":
|
|||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def init_loggers(cli_flags):
|
|
||||||
# d.py stuff
|
|
||||||
dpy_logger = logging.getLogger("discord")
|
|
||||||
dpy_logger.setLevel(logging.WARNING)
|
|
||||||
console = logging.StreamHandler()
|
|
||||||
console.setLevel(logging.WARNING)
|
|
||||||
dpy_logger.addHandler(console)
|
|
||||||
|
|
||||||
# Red stuff
|
|
||||||
|
|
||||||
logger = logging.getLogger("red")
|
|
||||||
|
|
||||||
red_format = logging.Formatter(
|
|
||||||
"%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: %(message)s",
|
|
||||||
datefmt="[%d/%m/%Y %H:%M]",
|
|
||||||
)
|
|
||||||
|
|
||||||
stdout_handler = logging.StreamHandler(sys.stdout)
|
|
||||||
stdout_handler.setFormatter(red_format)
|
|
||||||
|
|
||||||
if cli_flags.debug:
|
|
||||||
os.environ["PYTHONASYNCIODEBUG"] = "1"
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
else:
|
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
from redbot.core.data_manager import core_data_path
|
|
||||||
|
|
||||||
logfile_path = core_data_path() / "red.log"
|
|
||||||
fhandler = logging.handlers.RotatingFileHandler(
|
|
||||||
filename=str(logfile_path), encoding="utf-8", mode="a", maxBytes=10 ** 7, backupCount=5
|
|
||||||
)
|
|
||||||
fhandler.setFormatter(red_format)
|
|
||||||
|
|
||||||
logger.addHandler(fhandler)
|
|
||||||
logger.addHandler(stdout_handler)
|
|
||||||
|
|
||||||
# Sentry stuff
|
|
||||||
sentry_logger = logging.getLogger("red.sentry")
|
|
||||||
sentry_logger.setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
return logger, sentry_logger
|
|
||||||
|
|
||||||
|
|
||||||
async def _get_prefix_and_token(red, indict):
|
async def _get_prefix_and_token(red, indict):
|
||||||
"""
|
"""
|
||||||
Again, please blame <@269933075037814786> for this.
|
Again, please blame <@269933075037814786> for this.
|
||||||
@@ -91,18 +54,18 @@ async def _get_prefix_and_token(red, indict):
|
|||||||
"""
|
"""
|
||||||
indict["token"] = await red.db.token()
|
indict["token"] = await red.db.token()
|
||||||
indict["prefix"] = await red.db.prefix()
|
indict["prefix"] = await red.db.prefix()
|
||||||
indict["enable_sentry"] = await red.db.enable_sentry()
|
|
||||||
|
|
||||||
|
|
||||||
def list_instances():
|
def list_instances():
|
||||||
if not config_file.exists():
|
if not data_manager.config_file.exists():
|
||||||
print(
|
print(
|
||||||
"No instances have been configured! Configure one "
|
"No instances have been configured! Configure one "
|
||||||
"using `redbot-setup` before trying to run the bot!"
|
"using `redbot-setup` before trying to run the bot!"
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
data = JsonIO(config_file)._load_json()
|
with data_manager.config_file.open(encoding="utf-8") as fs:
|
||||||
|
data = json.load(fs)
|
||||||
text = "Configured Instances:\n\n"
|
text = "Configured Instances:\n\n"
|
||||||
for instance_name in sorted(data.keys()):
|
for instance_name in sorted(data.keys()):
|
||||||
text += "{}\n".format(instance_name)
|
text += "{}\n".format(instance_name)
|
||||||
@@ -110,6 +73,11 @@ def list_instances():
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
async def sigterm_handler(red, log):
|
||||||
|
log.info("SIGTERM received. Quitting...")
|
||||||
|
await red.shutdown(restart=False)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
description = "Red V3"
|
description = "Red V3"
|
||||||
cli_flags = parse_cli_flags(sys.argv[1:])
|
cli_flags = parse_cli_flags(sys.argv[1:])
|
||||||
@@ -117,6 +85,7 @@ def main():
|
|||||||
list_instances()
|
list_instances()
|
||||||
elif cli_flags.version:
|
elif cli_flags.version:
|
||||||
print(description)
|
print(description)
|
||||||
|
print("Current Version: {}".format(__version__))
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif not cli_flags.instance_name and not cli_flags.no_instance:
|
elif not cli_flags.instance_name and not cli_flags.no_instance:
|
||||||
print("Error: No instance name was provided!")
|
print("Error: No instance name was provided!")
|
||||||
@@ -124,21 +93,40 @@ def main():
|
|||||||
if cli_flags.no_instance:
|
if cli_flags.no_instance:
|
||||||
print(
|
print(
|
||||||
"\033[1m"
|
"\033[1m"
|
||||||
"Warning: The data will be placed in a temporary folder and removed on next system reboot."
|
"Warning: The data will be placed in a temporary folder and removed on next system "
|
||||||
|
"reboot."
|
||||||
"\033[0m"
|
"\033[0m"
|
||||||
)
|
)
|
||||||
cli_flags.instance_name = "temporary_red"
|
cli_flags.instance_name = "temporary_red"
|
||||||
create_temp_config()
|
data_manager.create_temp_config()
|
||||||
load_basic_configuration(cli_flags.instance_name)
|
data_manager.load_basic_configuration(cli_flags.instance_name)
|
||||||
log, sentry_log = init_loggers(cli_flags)
|
redbot.logging.init_logging(
|
||||||
red = Red(cli_flags=cli_flags, description=description, pm_help=None)
|
level=cli_flags.logging_level, location=data_manager.core_data_path() / "logs"
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug("====Basic Config====")
|
||||||
|
log.debug("Data Path: %s", data_manager._base_data_path())
|
||||||
|
log.debug("Storage Type: %s", data_manager.storage_type())
|
||||||
|
|
||||||
|
red = Red(
|
||||||
|
cli_flags=cli_flags, description=description, dm_help=None, fetch_offline_members=True
|
||||||
|
)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_until_complete(red.maybe_update_config())
|
||||||
init_global_checks(red)
|
init_global_checks(red)
|
||||||
init_events(red, cli_flags)
|
init_events(red, cli_flags)
|
||||||
|
|
||||||
red.add_cog(Core(red))
|
red.add_cog(Core(red))
|
||||||
red.add_cog(CogManagerUI())
|
red.add_cog(CogManagerUI())
|
||||||
if cli_flags.dev:
|
if cli_flags.dev:
|
||||||
red.add_cog(Dev())
|
red.add_cog(Dev())
|
||||||
loop = asyncio.get_event_loop()
|
# noinspection PyProtectedMember
|
||||||
|
loop.run_until_complete(modlog._init())
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
bank._init()
|
||||||
|
|
||||||
|
if os.name == "posix":
|
||||||
|
loop.add_signal_handler(SIGTERM, lambda: asyncio.ensure_future(sigterm_handler(red, log)))
|
||||||
tmp_data = {}
|
tmp_data = {}
|
||||||
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
||||||
token = os.environ.get("RED_TOKEN", tmp_data["token"])
|
token = os.environ.get("RED_TOKEN", tmp_data["token"])
|
||||||
@@ -158,8 +146,6 @@ def main():
|
|||||||
if cli_flags.dry_run:
|
if cli_flags.dry_run:
|
||||||
loop.run_until_complete(red.http.close())
|
loop.run_until_complete(red.http.close())
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
if tmp_data["enable_sentry"]:
|
|
||||||
red.enable_sentry()
|
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(red.start(token, bot=True))
|
loop.run_until_complete(red.start(token, bot=True))
|
||||||
except discord.LoginFailure:
|
except discord.LoginFailure:
|
||||||
@@ -176,7 +162,6 @@ def main():
|
|||||||
red._shutdown_mode = ExitCodes.SHUTDOWN
|
red._shutdown_mode = ExitCodes.SHUTDOWN
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.critical("Fatal exception", exc_info=e)
|
log.critical("Fatal exception", exc_info=e)
|
||||||
sentry_log.critical("Fatal Exception", exc_info=e)
|
|
||||||
loop.run_until_complete(red.logout())
|
loop.run_until_complete(red.logout())
|
||||||
finally:
|
finally:
|
||||||
pending = asyncio.Task.all_tasks(loop=red.loop)
|
pending = asyncio.Task.all_tasks(loop=red.loop)
|
||||||
|
|||||||
@@ -20,14 +20,21 @@ GENERIC_FORBIDDEN = _(
|
|||||||
)
|
)
|
||||||
|
|
||||||
HIERARCHY_ISSUE = _(
|
HIERARCHY_ISSUE = _(
|
||||||
"I tried to add {role.name} to {member.display_name} but that role"
|
"I tried to {verb} {role.name} to {member.display_name} but that role"
|
||||||
" is higher than my highest role in the Discord hierarchy so I was"
|
" is higher than my highest role in the Discord hierarchy so I was"
|
||||||
" unable to successfully add it. Please give me a higher role and "
|
" unable to successfully add it. Please give me a higher role and "
|
||||||
"try again."
|
"try again."
|
||||||
)
|
)
|
||||||
|
|
||||||
USER_HIERARCHY_ISSUE = _(
|
USER_HIERARCHY_ISSUE = _(
|
||||||
"I tried to add {role.name} to {member.display_name} but that role"
|
"I tried to {verb} {role.name} to {member.display_name} but that role"
|
||||||
|
" is higher than your highest role in the Discord hierarchy so I was"
|
||||||
|
" unable to successfully add it. Please get a higher role and "
|
||||||
|
"try again."
|
||||||
|
)
|
||||||
|
|
||||||
|
ROLE_USER_HIERARCHY_ISSUE = _(
|
||||||
|
"I tried to edit {role.name} but that role"
|
||||||
" is higher than your highest role in the Discord hierarchy so I was"
|
" is higher than your highest role in the Discord hierarchy so I was"
|
||||||
" unable to successfully add it. Please get a higher role and "
|
" unable to successfully add it. Please get a higher role and "
|
||||||
"try again."
|
"try again."
|
||||||
@@ -59,7 +66,7 @@ class Admin(commands.Cog):
|
|||||||
|
|
||||||
self.__current_announcer = None
|
self.__current_announcer = None
|
||||||
|
|
||||||
def __unload(self):
|
def cog_unload(self):
|
||||||
try:
|
try:
|
||||||
self.__current_announcer.cancel()
|
self.__current_announcer.cancel()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -104,7 +111,9 @@ class Admin(commands.Cog):
|
|||||||
await member.add_roles(role)
|
await member.add_roles(role)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
if not self.pass_hierarchy_check(ctx, role):
|
if not self.pass_hierarchy_check(ctx, role):
|
||||||
await self.complain(ctx, T_(HIERARCHY_ISSUE), role=role, member=member)
|
await self.complain(
|
||||||
|
ctx, T_(HIERARCHY_ISSUE), role=role, member=member, verb=_("add")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
|
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
|
||||||
else:
|
else:
|
||||||
@@ -119,7 +128,9 @@ class Admin(commands.Cog):
|
|||||||
await member.remove_roles(role)
|
await member.remove_roles(role)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
if not self.pass_hierarchy_check(ctx, role):
|
if not self.pass_hierarchy_check(ctx, role):
|
||||||
await self.complain(ctx, T_(HIERARCHY_ISSUE), role=role, member=member)
|
await self.complain(
|
||||||
|
ctx, T_(HIERARCHY_ISSUE), role=role, member=member, verb=_("remove")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
|
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
|
||||||
else:
|
else:
|
||||||
@@ -145,7 +156,9 @@ class Admin(commands.Cog):
|
|||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self._addrole(ctx, user, rolename)
|
await self._addrole(ctx, user, rolename)
|
||||||
else:
|
else:
|
||||||
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE), member=ctx.author, role=rolename)
|
await self.complain(
|
||||||
|
ctx, T_(USER_HIERARCHY_ISSUE), member=user, role=rolename, verb=_("add")
|
||||||
|
)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -163,7 +176,9 @@ class Admin(commands.Cog):
|
|||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self._removerole(ctx, user, rolename)
|
await self._removerole(ctx, user, rolename)
|
||||||
else:
|
else:
|
||||||
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE))
|
await self.complain(
|
||||||
|
ctx, T_(USER_HIERARCHY_ISSUE), member=user, role=rolename, verb=_("remove")
|
||||||
|
)
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -190,7 +205,7 @@ class Admin(commands.Cog):
|
|||||||
reason = "{}({}) changed the colour of role '{}'".format(author.name, author.id, role.name)
|
reason = "{}({}) changed the colour of role '{}'".format(author.name, author.id, role.name)
|
||||||
|
|
||||||
if not self.pass_user_hierarchy_check(ctx, role):
|
if not self.pass_user_hierarchy_check(ctx, role):
|
||||||
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE))
|
await self.complain(ctx, T_(ROLE_USER_HIERARCHY_ISSUE), role=role)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -218,7 +233,7 @@ class Admin(commands.Cog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not self.pass_user_hierarchy_check(ctx, role):
|
if not self.pass_user_hierarchy_check(ctx, role):
|
||||||
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE))
|
await self.complain(ctx, T_(ROLE_USER_HIERARCHY_ISSUE), role=role)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -300,7 +315,7 @@ class Admin(commands.Cog):
|
|||||||
valid_role_ids = set(r.id for r in valid_roles)
|
valid_role_ids = set(r.id for r in valid_roles)
|
||||||
|
|
||||||
if selfrole_ids != valid_role_ids:
|
if selfrole_ids != valid_role_ids:
|
||||||
await self.conf.guild(guild).selfroles.set(valid_role_ids)
|
await self.conf.guild(guild).selfroles.set(list(valid_role_ids))
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return valid_roles
|
return valid_roles
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class MemberDefaultAuthor(commands.Converter):
|
|||||||
|
|
||||||
class SelfRole(commands.Converter):
|
class SelfRole(commands.Converter):
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> discord.Role:
|
async def convert(self, ctx: commands.Context, arg: str) -> discord.Role:
|
||||||
admin = ctx.command.instance
|
admin = ctx.command.cog
|
||||||
if admin is None:
|
if admin is None:
|
||||||
raise commands.BadArgument(_("The Admin cog is not loaded."))
|
raise commands.BadArgument(_("The Admin cog is not loaded."))
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: red-discordbot\n"
|
"Project-Id-Version: red-discordbot\n"
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
"POT-Creation-Date: 2019-07-05 22:33+0200\n"
|
||||||
"PO-Revision-Date: 2018-04-15 16:41-0400\n"
|
"PO-Revision-Date: 2019-07-06 16:12\n"
|
||||||
"Last-Translator: Kowlin <boxedpp@gmail.com>\n"
|
"Last-Translator: Robert Jansen (Kowlin)\n"
|
||||||
"Language-Team: Arabic\n"
|
"Language-Team: Arabic\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
"Generated-By: redgettext 3.0\n"
|
||||||
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
|
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
|
||||||
"X-Generator: crowdin.com\n"
|
"X-Generator: crowdin.com\n"
|
||||||
"X-Crowdin-Project: red-discordbot\n"
|
"X-Crowdin-Project: red-discordbot\n"
|
||||||
@@ -16,3 +16,171 @@ msgstr ""
|
|||||||
"X-Crowdin-File: /cogs/admin/locales/messages.pot\n"
|
"X-Crowdin-File: /cogs/admin/locales/messages.pot\n"
|
||||||
"Language: ar_SA\n"
|
"Language: ar_SA\n"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:17
|
||||||
|
msgid "I attempted to do something that Discord denied me permissions for. Your command failed to successfully complete."
|
||||||
|
msgstr "حاولت القيام بشيء لا أملك تصريح من ديسكورد لفعله. فشل أمرك في إكمال نجاحه."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:22
|
||||||
|
msgid "I tried to {verb} {role.name} to {member.display_name} but that role is higher than my highest role in the Discord hierarchy so I was unable to successfully add it. Please give me a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:29
|
||||||
|
msgid "I tried to {verb} {role.name} to {member.display_name} but that role is higher than your highest role in the Discord hierarchy so I was unable to successfully add it. Please get a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:36
|
||||||
|
msgid "I tried to edit {role.name} but that role is higher than your highest role in the Discord hierarchy so I was unable to successfully add it. Please get a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:43
|
||||||
|
msgid "I am already announcing something. If you would like to make a different announcement please use `{prefix}announce cancel` first."
|
||||||
|
msgstr "أنا بالفعل أُعلن شيئاً. إذا كنت ترغب في إصدار إعلان مختلف الرجاء إستخدام `{prefix}announce cancel` أولاً."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:53
|
||||||
|
#, docstring
|
||||||
|
msgid "A collection of server administration utilities."
|
||||||
|
msgstr "مجموعة من أدوات إدارة السيرفر."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:115 redbot/cogs/admin/admin.py:160
|
||||||
|
msgid "add"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:121
|
||||||
|
msgid "I successfully added {role.name} to {member.display_name}"
|
||||||
|
msgstr "لقد قمت بنجاح بإضافة {role.name} إلى {member.display_name}"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:132 redbot/cogs/admin/admin.py:180
|
||||||
|
msgid "remove"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:138
|
||||||
|
msgid "I successfully removed {role.name} from {member.display_name}"
|
||||||
|
msgstr "لقد قمت بنجاح بإزالة {role.name} من {member.display_name}"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:149
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to a user.\\n\\n If user is left blank it defaults to the author of the command.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:169
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a role from a user.\\n\\n If user is left blank it defaults to the author of the command.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:187
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit role settings."
|
||||||
|
msgstr "تعديل إعدادات الرتبة."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:194
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit a role's colour.\\n\\n Use double quotes if the role contains spaces.\\n Colour must be in hexadecimal format.\\n [Online colour picker](http://www.w3schools.com/colors/colors_picker.asp)\\n\\n Examples:\\n `[p]editrole colour \\\"The Transistor\\\" #ff0000`\\n `[p]editrole colour Test #ff9900`\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:217 redbot/cogs/admin/admin.py:245
|
||||||
|
msgid "Done."
|
||||||
|
msgstr "تم."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:222
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit a role's name.\\n\\n Use double quotes if the role or the name contain spaces.\\n\\n Examples:\\n `[p]editrole name \\\"The Transistor\\\" Test`\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:250
|
||||||
|
#, docstring
|
||||||
|
msgid "Announce a message to all servers the bot is in."
|
||||||
|
msgstr "الإعلان عن رسالة إلى جميع السيرفرات المتواجد بها البوت."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:257
|
||||||
|
msgid "The announcement has begun."
|
||||||
|
msgstr "تم البدء في الإعلان."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:265
|
||||||
|
#, docstring
|
||||||
|
msgid "Cancel a running announce."
|
||||||
|
msgstr "إلغاء إعلان جارٍ."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:271
|
||||||
|
msgid "The current announcement has been cancelled."
|
||||||
|
msgstr "تم إلغاء الإعلان الحالي."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:277
|
||||||
|
#, docstring
|
||||||
|
msgid "Change the channel to which the bot makes announcements."
|
||||||
|
msgstr "تعديل القناة التي يستخدمها البوت للإعلانات."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:283
|
||||||
|
msgid "The announcement channel has been set to {channel.mention}"
|
||||||
|
msgstr "تم تحديث قناة الإعلانات إلى {channel.mention}"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:290
|
||||||
|
#, docstring
|
||||||
|
msgid "Toggle announcements being enabled this server."
|
||||||
|
msgstr "تم تمكين الإعلانات في السيرفر."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:296
|
||||||
|
msgid "The server {guild.name} will receive announcements."
|
||||||
|
msgstr "سيرفر {guild.name} سيتلقى إعلانات الآن."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:300
|
||||||
|
msgid "The server {guild.name} will not receive announcements."
|
||||||
|
msgstr "سيرفر {guild.name} لن يتلقى إعلانات."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:326
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to yourself.\\n\\n Server admins must have configured the role as user settable.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:337
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a selfrole from yourself.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:347
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to the list of available selfroles.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:355 redbot/cogs/admin/admin.py:367
|
||||||
|
msgid "The selfroles list has been successfully modified."
|
||||||
|
msgstr "تم تحديث/تعديل قائمة الرتب الذاتية بنجاح."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:360
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a role from the list of available selfroles.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:371
|
||||||
|
#, docstring
|
||||||
|
msgid "\\n Lists all available selfroles.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:377
|
||||||
|
msgid "Available Selfroles:\\n{selfroles}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:394
|
||||||
|
#, docstring
|
||||||
|
msgid "Lock a bot to its current servers only."
|
||||||
|
msgstr "قفل البوت للسيرفر الحالي فقط."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:399
|
||||||
|
msgid "The bot is no longer serverlocked."
|
||||||
|
msgstr "لم يعد البوت مقفول للسيرفر الحالي فقط."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:401
|
||||||
|
msgid "The bot is now serverlocked."
|
||||||
|
msgstr "لقد تم قفل البوت للسيرفر الحالي فقط."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/announcer.py:70
|
||||||
|
msgid "I could not announce to server: {server.id}"
|
||||||
|
msgstr "لم أتمكن من الإعلان في السيرفر: {server.id}"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/converters.py:25
|
||||||
|
msgid "The Admin cog is not loaded."
|
||||||
|
msgstr "لم يتم تحميل وحدة الأدمن cog."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/converters.py:34
|
||||||
|
msgid "The provided role is not a valid selfrole."
|
||||||
|
msgstr "الرتبة المذكورة ليست على قائمة الرتب الذاتية."
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: red-discordbot\n"
|
"Project-Id-Version: red-discordbot\n"
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
"POT-Creation-Date: 2019-07-05 22:33+0200\n"
|
||||||
"PO-Revision-Date: 2018-04-15 16:42-0400\n"
|
"PO-Revision-Date: 2019-07-06 16:12\n"
|
||||||
"Last-Translator: Kowlin <boxedpp@gmail.com>\n"
|
"Last-Translator: Robert Jansen (Kowlin)\n"
|
||||||
"Language-Team: Bulgarian\n"
|
"Language-Team: Bulgarian\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
"Generated-By: redgettext 3.0\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Generator: crowdin.com\n"
|
"X-Generator: crowdin.com\n"
|
||||||
"X-Crowdin-Project: red-discordbot\n"
|
"X-Crowdin-Project: red-discordbot\n"
|
||||||
@@ -16,3 +16,171 @@ msgstr ""
|
|||||||
"X-Crowdin-File: /cogs/admin/locales/messages.pot\n"
|
"X-Crowdin-File: /cogs/admin/locales/messages.pot\n"
|
||||||
"Language: bg_BG\n"
|
"Language: bg_BG\n"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:17
|
||||||
|
msgid "I attempted to do something that Discord denied me permissions for. Your command failed to successfully complete."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:22
|
||||||
|
msgid "I tried to {verb} {role.name} to {member.display_name} but that role is higher than my highest role in the Discord hierarchy so I was unable to successfully add it. Please give me a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:29
|
||||||
|
msgid "I tried to {verb} {role.name} to {member.display_name} but that role is higher than your highest role in the Discord hierarchy so I was unable to successfully add it. Please get a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:36
|
||||||
|
msgid "I tried to edit {role.name} but that role is higher than your highest role in the Discord hierarchy so I was unable to successfully add it. Please get a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:43
|
||||||
|
msgid "I am already announcing something. If you would like to make a different announcement please use `{prefix}announce cancel` first."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:53
|
||||||
|
#, docstring
|
||||||
|
msgid "A collection of server administration utilities."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:115 redbot/cogs/admin/admin.py:160
|
||||||
|
msgid "add"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:121
|
||||||
|
msgid "I successfully added {role.name} to {member.display_name}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:132 redbot/cogs/admin/admin.py:180
|
||||||
|
msgid "remove"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:138
|
||||||
|
msgid "I successfully removed {role.name} from {member.display_name}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:149
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to a user.\\n\\n If user is left blank it defaults to the author of the command.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:169
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a role from a user.\\n\\n If user is left blank it defaults to the author of the command.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:187
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit role settings."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:194
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit a role's colour.\\n\\n Use double quotes if the role contains spaces.\\n Colour must be in hexadecimal format.\\n [Online colour picker](http://www.w3schools.com/colors/colors_picker.asp)\\n\\n Examples:\\n `[p]editrole colour \\\"The Transistor\\\" #ff0000`\\n `[p]editrole colour Test #ff9900`\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:217 redbot/cogs/admin/admin.py:245
|
||||||
|
msgid "Done."
|
||||||
|
msgstr "Завършено."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:222
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit a role's name.\\n\\n Use double quotes if the role or the name contain spaces.\\n\\n Examples:\\n `[p]editrole name \\\"The Transistor\\\" Test`\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:250
|
||||||
|
#, docstring
|
||||||
|
msgid "Announce a message to all servers the bot is in."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:257
|
||||||
|
msgid "The announcement has begun."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:265
|
||||||
|
#, docstring
|
||||||
|
msgid "Cancel a running announce."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:271
|
||||||
|
msgid "The current announcement has been cancelled."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:277
|
||||||
|
#, docstring
|
||||||
|
msgid "Change the channel to which the bot makes announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:283
|
||||||
|
msgid "The announcement channel has been set to {channel.mention}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:290
|
||||||
|
#, docstring
|
||||||
|
msgid "Toggle announcements being enabled this server."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:296
|
||||||
|
msgid "The server {guild.name} will receive announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:300
|
||||||
|
msgid "The server {guild.name} will not receive announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:326
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to yourself.\\n\\n Server admins must have configured the role as user settable.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:337
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a selfrole from yourself.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:347
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to the list of available selfroles.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:355 redbot/cogs/admin/admin.py:367
|
||||||
|
msgid "The selfroles list has been successfully modified."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:360
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a role from the list of available selfroles.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:371
|
||||||
|
#, docstring
|
||||||
|
msgid "\\n Lists all available selfroles.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:377
|
||||||
|
msgid "Available Selfroles:\\n{selfroles}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:394
|
||||||
|
#, docstring
|
||||||
|
msgid "Lock a bot to its current servers only."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:399
|
||||||
|
msgid "The bot is no longer serverlocked."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:401
|
||||||
|
msgid "The bot is now serverlocked."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/announcer.py:70
|
||||||
|
msgid "I could not announce to server: {server.id}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/converters.py:25
|
||||||
|
msgid "The Admin cog is not loaded."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/converters.py:34
|
||||||
|
msgid "The provided role is not a valid selfrole."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
186
redbot/cogs/admin/locales/cs-CZ.po
Normal file
186
redbot/cogs/admin/locales/cs-CZ.po
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: red-discordbot\n"
|
||||||
|
"POT-Creation-Date: 2019-07-05 22:33+0200\n"
|
||||||
|
"PO-Revision-Date: 2019-07-14 02:15\n"
|
||||||
|
"Last-Translator: Robert Jansen (Kowlin)\n"
|
||||||
|
"Language-Team: Czech\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: redgettext 3.0\n"
|
||||||
|
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
|
||||||
|
"X-Generator: crowdin.com\n"
|
||||||
|
"X-Crowdin-Project: red-discordbot\n"
|
||||||
|
"X-Crowdin-Language: cs\n"
|
||||||
|
"X-Crowdin-File: /cogs/admin/locales/messages.pot\n"
|
||||||
|
"Language: cs_CZ\n"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:17
|
||||||
|
msgid "I attempted to do something that Discord denied me permissions for. Your command failed to successfully complete."
|
||||||
|
msgstr "Pokusil jsem se udělat něco, co mi Discord odepřel. Tvůj příkaz se nepodažilo úspěšně dokončit."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:22
|
||||||
|
msgid "I tried to {verb} {role.name} to {member.display_name} but that role is higher than my highest role in the Discord hierarchy so I was unable to successfully add it. Please give me a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:29
|
||||||
|
msgid "I tried to {verb} {role.name} to {member.display_name} but that role is higher than your highest role in the Discord hierarchy so I was unable to successfully add it. Please get a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:36
|
||||||
|
msgid "I tried to edit {role.name} but that role is higher than your highest role in the Discord hierarchy so I was unable to successfully add it. Please get a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:43
|
||||||
|
msgid "I am already announcing something. If you would like to make a different announcement please use `{prefix}announce cancel` first."
|
||||||
|
msgstr "Už něco oznamuji. Pokud bys rád provedl jiné oznámení, nejdříve použij `{prefix}announce cancel`."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:53
|
||||||
|
#, docstring
|
||||||
|
msgid "A collection of server administration utilities."
|
||||||
|
msgstr "Kolekce nástrojů pro správu serveru."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:115 redbot/cogs/admin/admin.py:160
|
||||||
|
msgid "add"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:121
|
||||||
|
msgid "I successfully added {role.name} to {member.display_name}"
|
||||||
|
msgstr "Úspěšně jsem přidal {role.name} k {member.display_name}"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:132 redbot/cogs/admin/admin.py:180
|
||||||
|
msgid "remove"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:138
|
||||||
|
msgid "I successfully removed {role.name} from {member.display_name}"
|
||||||
|
msgstr "Úspěšně jsem odstranil {role.name} od {member.display_name}"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:149
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to a user.\\n\\n If user is left blank it defaults to the author of the command.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:169
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a role from a user.\\n\\n If user is left blank it defaults to the author of the command.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:187
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit role settings."
|
||||||
|
msgstr "Upraví nastavení role."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:194
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit a role's colour.\\n\\n Use double quotes if the role contains spaces.\\n Colour must be in hexadecimal format.\\n [Online colour picker](http://www.w3schools.com/colors/colors_picker.asp)\\n\\n Examples:\\n `[p]editrole colour \\\"The Transistor\\\" #ff0000`\\n `[p]editrole colour Test #ff9900`\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:217 redbot/cogs/admin/admin.py:245
|
||||||
|
msgid "Done."
|
||||||
|
msgstr "Hotovo."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:222
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit a role's name.\\n\\n Use double quotes if the role or the name contain spaces.\\n\\n Examples:\\n `[p]editrole name \\\"The Transistor\\\" Test`\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:250
|
||||||
|
#, docstring
|
||||||
|
msgid "Announce a message to all servers the bot is in."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:257
|
||||||
|
msgid "The announcement has begun."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:265
|
||||||
|
#, docstring
|
||||||
|
msgid "Cancel a running announce."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:271
|
||||||
|
msgid "The current announcement has been cancelled."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:277
|
||||||
|
#, docstring
|
||||||
|
msgid "Change the channel to which the bot makes announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:283
|
||||||
|
msgid "The announcement channel has been set to {channel.mention}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:290
|
||||||
|
#, docstring
|
||||||
|
msgid "Toggle announcements being enabled this server."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:296
|
||||||
|
msgid "The server {guild.name} will receive announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:300
|
||||||
|
msgid "The server {guild.name} will not receive announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:326
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to yourself.\\n\\n Server admins must have configured the role as user settable.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:337
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a selfrole from yourself.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:347
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to the list of available selfroles.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:355 redbot/cogs/admin/admin.py:367
|
||||||
|
msgid "The selfroles list has been successfully modified."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:360
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a role from the list of available selfroles.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:371
|
||||||
|
#, docstring
|
||||||
|
msgid "\\n Lists all available selfroles.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:377
|
||||||
|
msgid "Available Selfroles:\\n{selfroles}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:394
|
||||||
|
#, docstring
|
||||||
|
msgid "Lock a bot to its current servers only."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:399
|
||||||
|
msgid "The bot is no longer serverlocked."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:401
|
||||||
|
msgid "The bot is now serverlocked."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/announcer.py:70
|
||||||
|
msgid "I could not announce to server: {server.id}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/converters.py:25
|
||||||
|
msgid "The Admin cog is not loaded."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/converters.py:34
|
||||||
|
msgid "The provided role is not a valid selfrole."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: red-discordbot\n"
|
"Project-Id-Version: red-discordbot\n"
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
"POT-Creation-Date: 2019-07-05 22:33+0200\n"
|
||||||
"PO-Revision-Date: 2018-04-15 16:42-0400\n"
|
"PO-Revision-Date: 2019-07-06 16:12\n"
|
||||||
"Last-Translator: Kowlin <boxedpp@gmail.com>\n"
|
"Last-Translator: Robert Jansen (Kowlin)\n"
|
||||||
"Language-Team: Danish\n"
|
"Language-Team: Danish\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
"Generated-By: redgettext 3.0\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Generator: crowdin.com\n"
|
"X-Generator: crowdin.com\n"
|
||||||
"X-Crowdin-Project: red-discordbot\n"
|
"X-Crowdin-Project: red-discordbot\n"
|
||||||
@@ -16,3 +16,171 @@ msgstr ""
|
|||||||
"X-Crowdin-File: /cogs/admin/locales/messages.pot\n"
|
"X-Crowdin-File: /cogs/admin/locales/messages.pot\n"
|
||||||
"Language: da_DK\n"
|
"Language: da_DK\n"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:17
|
||||||
|
msgid "I attempted to do something that Discord denied me permissions for. Your command failed to successfully complete."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:22
|
||||||
|
msgid "I tried to {verb} {role.name} to {member.display_name} but that role is higher than my highest role in the Discord hierarchy so I was unable to successfully add it. Please give me a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:29
|
||||||
|
msgid "I tried to {verb} {role.name} to {member.display_name} but that role is higher than your highest role in the Discord hierarchy so I was unable to successfully add it. Please get a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:36
|
||||||
|
msgid "I tried to edit {role.name} but that role is higher than your highest role in the Discord hierarchy so I was unable to successfully add it. Please get a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:43
|
||||||
|
msgid "I am already announcing something. If you would like to make a different announcement please use `{prefix}announce cancel` first."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:53
|
||||||
|
#, docstring
|
||||||
|
msgid "A collection of server administration utilities."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:115 redbot/cogs/admin/admin.py:160
|
||||||
|
msgid "add"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:121
|
||||||
|
msgid "I successfully added {role.name} to {member.display_name}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:132 redbot/cogs/admin/admin.py:180
|
||||||
|
msgid "remove"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:138
|
||||||
|
msgid "I successfully removed {role.name} from {member.display_name}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:149
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to a user.\\n\\n If user is left blank it defaults to the author of the command.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:169
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a role from a user.\\n\\n If user is left blank it defaults to the author of the command.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:187
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit role settings."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:194
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit a role's colour.\\n\\n Use double quotes if the role contains spaces.\\n Colour must be in hexadecimal format.\\n [Online colour picker](http://www.w3schools.com/colors/colors_picker.asp)\\n\\n Examples:\\n `[p]editrole colour \\\"The Transistor\\\" #ff0000`\\n `[p]editrole colour Test #ff9900`\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:217 redbot/cogs/admin/admin.py:245
|
||||||
|
msgid "Done."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:222
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit a role's name.\\n\\n Use double quotes if the role or the name contain spaces.\\n\\n Examples:\\n `[p]editrole name \\\"The Transistor\\\" Test`\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:250
|
||||||
|
#, docstring
|
||||||
|
msgid "Announce a message to all servers the bot is in."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:257
|
||||||
|
msgid "The announcement has begun."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:265
|
||||||
|
#, docstring
|
||||||
|
msgid "Cancel a running announce."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:271
|
||||||
|
msgid "The current announcement has been cancelled."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:277
|
||||||
|
#, docstring
|
||||||
|
msgid "Change the channel to which the bot makes announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:283
|
||||||
|
msgid "The announcement channel has been set to {channel.mention}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:290
|
||||||
|
#, docstring
|
||||||
|
msgid "Toggle announcements being enabled this server."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:296
|
||||||
|
msgid "The server {guild.name} will receive announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:300
|
||||||
|
msgid "The server {guild.name} will not receive announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:326
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to yourself.\\n\\n Server admins must have configured the role as user settable.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:337
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a selfrole from yourself.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:347
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to the list of available selfroles.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:355 redbot/cogs/admin/admin.py:367
|
||||||
|
msgid "The selfroles list has been successfully modified."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:360
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a role from the list of available selfroles.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:371
|
||||||
|
#, docstring
|
||||||
|
msgid "\\n Lists all available selfroles.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:377
|
||||||
|
msgid "Available Selfroles:\\n{selfroles}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:394
|
||||||
|
#, docstring
|
||||||
|
msgid "Lock a bot to its current servers only."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:399
|
||||||
|
msgid "The bot is no longer serverlocked."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:401
|
||||||
|
msgid "The bot is now serverlocked."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/announcer.py:70
|
||||||
|
msgid "I could not announce to server: {server.id}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/converters.py:25
|
||||||
|
msgid "The Admin cog is not loaded."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/converters.py:34
|
||||||
|
msgid "The provided role is not a valid selfrole."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: red-discordbot\n"
|
"Project-Id-Version: red-discordbot\n"
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
"POT-Creation-Date: 2019-07-05 22:33+0200\n"
|
||||||
"PO-Revision-Date: 2018-04-15 16:42-0400\n"
|
"PO-Revision-Date: 2019-07-06 16:12\n"
|
||||||
"Last-Translator: Kowlin <boxedpp@gmail.com>\n"
|
"Last-Translator: Robert Jansen (Kowlin)\n"
|
||||||
"Language-Team: German\n"
|
"Language-Team: German\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
"Generated-By: redgettext 3.0\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Generator: crowdin.com\n"
|
"X-Generator: crowdin.com\n"
|
||||||
"X-Crowdin-Project: red-discordbot\n"
|
"X-Crowdin-Project: red-discordbot\n"
|
||||||
@@ -16,3 +16,171 @@ msgstr ""
|
|||||||
"X-Crowdin-File: /cogs/admin/locales/messages.pot\n"
|
"X-Crowdin-File: /cogs/admin/locales/messages.pot\n"
|
||||||
"Language: de_DE\n"
|
"Language: de_DE\n"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:17
|
||||||
|
msgid "I attempted to do something that Discord denied me permissions for. Your command failed to successfully complete."
|
||||||
|
msgstr "Ich habe versucht etwas zu tun, für das mir Discord die Befugnis verweigerte. Dein Befehl konnte nicht erfolgreich ausgeführt werden."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:22
|
||||||
|
msgid "I tried to {verb} {role.name} to {member.display_name} but that role is higher than my highest role in the Discord hierarchy so I was unable to successfully add it. Please give me a higher role and try again."
|
||||||
|
msgstr "Ich habe versucht {member.display_name} er Rolle: {role.name} {verb}, aber diese Rolle hat höhere Rechte als ich. Also war es nicht möglich die Rolle erfolgreich hinzuzufügen. Bitte geb mir eine höhere Rolle und versuche es erneut."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:29
|
||||||
|
msgid "I tried to {verb} {role.name} to {member.display_name} but that role is higher than your highest role in the Discord hierarchy so I was unable to successfully add it. Please get a higher role and try again."
|
||||||
|
msgstr "Ich habe versucht die Rolle {role.name} an {member.display_name} zu {verb}, aber die Rolle ist höher als meine Rolle in der Discord Hierarchie, daher war es mir nicht möglich sie hinzuzufügen. Bitte gib mir eine höhere Rolle und versuche es erneut."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:36
|
||||||
|
msgid "I tried to edit {role.name} but that role is higher than your highest role in the Discord hierarchy so I was unable to successfully add it. Please get a higher role and try again."
|
||||||
|
msgstr "Ich habe versucht {role.name} zu bearbeiten, aber die Rolle ist höher als deine höhste Rolle im Discord. Also konnte ich dies nicht erfolgreich hinzufügen. Versuche es mit einer höheren Rolle erneut."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:43
|
||||||
|
msgid "I am already announcing something. If you would like to make a different announcement please use `{prefix}announce cancel` first."
|
||||||
|
msgstr "Ich kündige bereits etwas an. Wenn du eine andere Ankündigung machen willst, benutze bitte zuerst `{prefix}announce cancel`."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:53
|
||||||
|
#, docstring
|
||||||
|
msgid "A collection of server administration utilities."
|
||||||
|
msgstr "Eine Sammlung von administrativen Server-Verwaltungsprogramme."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:115 redbot/cogs/admin/admin.py:160
|
||||||
|
msgid "add"
|
||||||
|
msgstr "hinzuzufügen"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:121
|
||||||
|
msgid "I successfully added {role.name} to {member.display_name}"
|
||||||
|
msgstr "Ich habe erfolgreich {role.name} zu {member.display_name} hinzugefügt"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:132 redbot/cogs/admin/admin.py:180
|
||||||
|
msgid "remove"
|
||||||
|
msgstr "entfernen"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:138
|
||||||
|
msgid "I successfully removed {role.name} from {member.display_name}"
|
||||||
|
msgstr "Ich habe erfolgreich {role.name} von {member.display_name} entfernt"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:149
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to a user.\\n\\n If user is left blank it defaults to the author of the command.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:169
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a role from a user.\\n\\n If user is left blank it defaults to the author of the command.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:187
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit role settings."
|
||||||
|
msgstr "Bearbeiten der Rollen Einstellungen."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:194
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit a role's colour.\\n\\n Use double quotes if the role contains spaces.\\n Colour must be in hexadecimal format.\\n [Online colour picker](http://www.w3schools.com/colors/colors_picker.asp)\\n\\n Examples:\\n `[p]editrole colour \\\"The Transistor\\\" #ff0000`\\n `[p]editrole colour Test #ff9900`\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:217 redbot/cogs/admin/admin.py:245
|
||||||
|
msgid "Done."
|
||||||
|
msgstr "Erledigt."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:222
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit a role's name.\\n\\n Use double quotes if the role or the name contain spaces.\\n\\n Examples:\\n `[p]editrole name \\\"The Transistor\\\" Test`\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:250
|
||||||
|
#, docstring
|
||||||
|
msgid "Announce a message to all servers the bot is in."
|
||||||
|
msgstr "Mache eine Ankündigung auf allen Servern auf denen der Bot ist."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:257
|
||||||
|
msgid "The announcement has begun."
|
||||||
|
msgstr "Die Ankündigung hat begonnen."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:265
|
||||||
|
#, docstring
|
||||||
|
msgid "Cancel a running announce."
|
||||||
|
msgstr "Breche eine laufende Ankündigung ab."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:271
|
||||||
|
msgid "The current announcement has been cancelled."
|
||||||
|
msgstr "Die aktuelle Ankündigung wurde abgebrochen."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:277
|
||||||
|
#, docstring
|
||||||
|
msgid "Change the channel to which the bot makes announcements."
|
||||||
|
msgstr "Ändere den Kanal in dem der Bot Ankündigungen macht."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:283
|
||||||
|
msgid "The announcement channel has been set to {channel.mention}"
|
||||||
|
msgstr "Der für Ankündigungen gewählte Kanal wurde festgelegt auf {channel.mention}"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:290
|
||||||
|
#, docstring
|
||||||
|
msgid "Toggle announcements being enabled this server."
|
||||||
|
msgstr "Schaltet Ankündigungen auf diesem Server ein oder aus."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:296
|
||||||
|
msgid "The server {guild.name} will receive announcements."
|
||||||
|
msgstr "Der Server {guild.name} wird Ankündigunen erhalten."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:300
|
||||||
|
msgid "The server {guild.name} will not receive announcements."
|
||||||
|
msgstr "Der Server {guild.name} wird keine Ankündigunen erhalten."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:326
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to yourself.\\n\\n Server admins must have configured the role as user settable.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:337
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a selfrole from yourself.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:347
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to the list of available selfroles.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:355 redbot/cogs/admin/admin.py:367
|
||||||
|
msgid "The selfroles list has been successfully modified."
|
||||||
|
msgstr "Die Liste selbst auswählbarer Rollen wurde erfolgreich bearbeitet."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:360
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a role from the list of available selfroles.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:371
|
||||||
|
#, docstring
|
||||||
|
msgid "\\n Lists all available selfroles.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:377
|
||||||
|
msgid "Available Selfroles:\\n{selfroles}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:394
|
||||||
|
#, docstring
|
||||||
|
msgid "Lock a bot to its current servers only."
|
||||||
|
msgstr "Sperrt den Bot neuen Servern beizutreten."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:399
|
||||||
|
msgid "The bot is no longer serverlocked."
|
||||||
|
msgstr "Der Bot ist nicht länger gesperrt neuen Servern beizutreten."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:401
|
||||||
|
msgid "The bot is now serverlocked."
|
||||||
|
msgstr "Der Bot ist jetzt gesperrt neuen Servern beizutreten."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/announcer.py:70
|
||||||
|
msgid "I could not announce to server: {server.id}"
|
||||||
|
msgstr "Ich konnte keine Ankündigung auf diesem Server machen: {server.id}"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/converters.py:25
|
||||||
|
msgid "The Admin cog is not loaded."
|
||||||
|
msgstr "Das Admin-Cog ist nicht geladen."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/converters.py:34
|
||||||
|
msgid "The provided role is not a valid selfrole."
|
||||||
|
msgstr "Die vorausgesetzte Rolle ist keine berechtigte selbst auswählbare Rolle."
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: red-discordbot\n"
|
"Project-Id-Version: red-discordbot\n"
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
"POT-Creation-Date: 2019-07-05 22:33+0200\n"
|
||||||
"PO-Revision-Date: 2018-04-15 16:42-0400\n"
|
"PO-Revision-Date: 2019-07-06 16:12\n"
|
||||||
"Last-Translator: Kowlin <boxedpp@gmail.com>\n"
|
"Last-Translator: Robert Jansen (Kowlin)\n"
|
||||||
"Language-Team: Greek\n"
|
"Language-Team: Greek\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
"Generated-By: redgettext 3.0\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Generator: crowdin.com\n"
|
"X-Generator: crowdin.com\n"
|
||||||
"X-Crowdin-Project: red-discordbot\n"
|
"X-Crowdin-Project: red-discordbot\n"
|
||||||
@@ -16,3 +16,171 @@ msgstr ""
|
|||||||
"X-Crowdin-File: /cogs/admin/locales/messages.pot\n"
|
"X-Crowdin-File: /cogs/admin/locales/messages.pot\n"
|
||||||
"Language: el_GR\n"
|
"Language: el_GR\n"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:17
|
||||||
|
msgid "I attempted to do something that Discord denied me permissions for. Your command failed to successfully complete."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:22
|
||||||
|
msgid "I tried to {verb} {role.name} to {member.display_name} but that role is higher than my highest role in the Discord hierarchy so I was unable to successfully add it. Please give me a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:29
|
||||||
|
msgid "I tried to {verb} {role.name} to {member.display_name} but that role is higher than your highest role in the Discord hierarchy so I was unable to successfully add it. Please get a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:36
|
||||||
|
msgid "I tried to edit {role.name} but that role is higher than your highest role in the Discord hierarchy so I was unable to successfully add it. Please get a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:43
|
||||||
|
msgid "I am already announcing something. If you would like to make a different announcement please use `{prefix}announce cancel` first."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:53
|
||||||
|
#, docstring
|
||||||
|
msgid "A collection of server administration utilities."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:115 redbot/cogs/admin/admin.py:160
|
||||||
|
msgid "add"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:121
|
||||||
|
msgid "I successfully added {role.name} to {member.display_name}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:132 redbot/cogs/admin/admin.py:180
|
||||||
|
msgid "remove"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:138
|
||||||
|
msgid "I successfully removed {role.name} from {member.display_name}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:149
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to a user.\\n\\n If user is left blank it defaults to the author of the command.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:169
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a role from a user.\\n\\n If user is left blank it defaults to the author of the command.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:187
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit role settings."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:194
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit a role's colour.\\n\\n Use double quotes if the role contains spaces.\\n Colour must be in hexadecimal format.\\n [Online colour picker](http://www.w3schools.com/colors/colors_picker.asp)\\n\\n Examples:\\n `[p]editrole colour \\\"The Transistor\\\" #ff0000`\\n `[p]editrole colour Test #ff9900`\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:217 redbot/cogs/admin/admin.py:245
|
||||||
|
msgid "Done."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:222
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit a role's name.\\n\\n Use double quotes if the role or the name contain spaces.\\n\\n Examples:\\n `[p]editrole name \\\"The Transistor\\\" Test`\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:250
|
||||||
|
#, docstring
|
||||||
|
msgid "Announce a message to all servers the bot is in."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:257
|
||||||
|
msgid "The announcement has begun."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:265
|
||||||
|
#, docstring
|
||||||
|
msgid "Cancel a running announce."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:271
|
||||||
|
msgid "The current announcement has been cancelled."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:277
|
||||||
|
#, docstring
|
||||||
|
msgid "Change the channel to which the bot makes announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:283
|
||||||
|
msgid "The announcement channel has been set to {channel.mention}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:290
|
||||||
|
#, docstring
|
||||||
|
msgid "Toggle announcements being enabled this server."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:296
|
||||||
|
msgid "The server {guild.name} will receive announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:300
|
||||||
|
msgid "The server {guild.name} will not receive announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:326
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to yourself.\\n\\n Server admins must have configured the role as user settable.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:337
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a selfrole from yourself.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:347
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to the list of available selfroles.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:355 redbot/cogs/admin/admin.py:367
|
||||||
|
msgid "The selfroles list has been successfully modified."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:360
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a role from the list of available selfroles.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:371
|
||||||
|
#, docstring
|
||||||
|
msgid "\\n Lists all available selfroles.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:377
|
||||||
|
msgid "Available Selfroles:\\n{selfroles}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:394
|
||||||
|
#, docstring
|
||||||
|
msgid "Lock a bot to its current servers only."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:399
|
||||||
|
msgid "The bot is no longer serverlocked."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:401
|
||||||
|
msgid "The bot is now serverlocked."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/announcer.py:70
|
||||||
|
msgid "I could not announce to server: {server.id}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/converters.py:25
|
||||||
|
msgid "The Admin cog is not loaded."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/converters.py:34
|
||||||
|
msgid "The provided role is not a valid selfrole."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: red-discordbot\n"
|
"Project-Id-Version: red-discordbot\n"
|
||||||
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
|
"POT-Creation-Date: 2019-07-05 22:33+0200\n"
|
||||||
"PO-Revision-Date: 2018-04-15 16:43-0400\n"
|
"PO-Revision-Date: 2019-07-14 02:15\n"
|
||||||
"Last-Translator: Kowlin <boxedpp@gmail.com>\n"
|
"Last-Translator: Robert Jansen (Kowlin)\n"
|
||||||
"Language-Team: Pirate English\n"
|
"Language-Team: Pirate English\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: ENCODING\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: pygettext.py 1.5\n"
|
"Generated-By: redgettext 3.0\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Generator: crowdin.com\n"
|
"X-Generator: crowdin.com\n"
|
||||||
"X-Crowdin-Project: red-discordbot\n"
|
"X-Crowdin-Project: red-discordbot\n"
|
||||||
@@ -16,3 +16,171 @@ msgstr ""
|
|||||||
"X-Crowdin-File: /cogs/admin/locales/messages.pot\n"
|
"X-Crowdin-File: /cogs/admin/locales/messages.pot\n"
|
||||||
"Language: en_PT\n"
|
"Language: en_PT\n"
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:17
|
||||||
|
msgid "I attempted to do something that Discord denied me permissions for. Your command failed to successfully complete."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:22
|
||||||
|
msgid "I tried to {verb} {role.name} to {member.display_name} but that role is higher than my highest role in the Discord hierarchy so I was unable to successfully add it. Please give me a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:29
|
||||||
|
msgid "I tried to {verb} {role.name} to {member.display_name} but that role is higher than your highest role in the Discord hierarchy so I was unable to successfully add it. Please get a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:36
|
||||||
|
msgid "I tried to edit {role.name} but that role is higher than your highest role in the Discord hierarchy so I was unable to successfully add it. Please get a higher role and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:43
|
||||||
|
msgid "I am already announcing something. If you would like to make a different announcement please use `{prefix}announce cancel` first."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:53
|
||||||
|
#, docstring
|
||||||
|
msgid "A collection of server administration utilities."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:115 redbot/cogs/admin/admin.py:160
|
||||||
|
msgid "add"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:121
|
||||||
|
msgid "I successfully added {role.name} to {member.display_name}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:132 redbot/cogs/admin/admin.py:180
|
||||||
|
msgid "remove"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:138
|
||||||
|
msgid "I successfully removed {role.name} from {member.display_name}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:149
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to a user.\\n\\n If user is left blank it defaults to the author of the command.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:169
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a role from a user.\\n\\n If user is left blank it defaults to the author of the command.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:187
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit role settings."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:194
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit a role's colour.\\n\\n Use double quotes if the role contains spaces.\\n Colour must be in hexadecimal format.\\n [Online colour picker](http://www.w3schools.com/colors/colors_picker.asp)\\n\\n Examples:\\n `[p]editrole colour \\\"The Transistor\\\" #ff0000`\\n `[p]editrole colour Test #ff9900`\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:217 redbot/cogs/admin/admin.py:245
|
||||||
|
msgid "Done."
|
||||||
|
msgstr "Done."
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:222
|
||||||
|
#, docstring
|
||||||
|
msgid "Edit a role's name.\\n\\n Use double quotes if the role or the name contain spaces.\\n\\n Examples:\\n `[p]editrole name \\\"The Transistor\\\" Test`\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:250
|
||||||
|
#, docstring
|
||||||
|
msgid "Announce a message to all servers the bot is in."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:257
|
||||||
|
msgid "The announcement has begun."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:265
|
||||||
|
#, docstring
|
||||||
|
msgid "Cancel a running announce."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:271
|
||||||
|
msgid "The current announcement has been cancelled."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:277
|
||||||
|
#, docstring
|
||||||
|
msgid "Change the channel to which the bot makes announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:283
|
||||||
|
msgid "The announcement channel has been set to {channel.mention}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:290
|
||||||
|
#, docstring
|
||||||
|
msgid "Toggle announcements being enabled this server."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:296
|
||||||
|
msgid "The server {guild.name} will receive announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:300
|
||||||
|
msgid "The server {guild.name} will not receive announcements."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:326
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to yourself.\\n\\n Server admins must have configured the role as user settable.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:337
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a selfrole from yourself.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:347
|
||||||
|
#, docstring
|
||||||
|
msgid "Add a role to the list of available selfroles.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:355 redbot/cogs/admin/admin.py:367
|
||||||
|
msgid "The selfroles list has been successfully modified."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:360
|
||||||
|
#, docstring
|
||||||
|
msgid "Remove a role from the list of available selfroles.\\n\\n NOTE: The role is case sensitive!\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:371
|
||||||
|
#, docstring
|
||||||
|
msgid "\\n Lists all available selfroles.\\n "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:377
|
||||||
|
msgid "Available Selfroles:\\n{selfroles}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:394
|
||||||
|
#, docstring
|
||||||
|
msgid "Lock a bot to its current servers only."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:399
|
||||||
|
msgid "The bot is no longer serverlocked."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/admin.py:401
|
||||||
|
msgid "The bot is now serverlocked."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/announcer.py:70
|
||||||
|
msgid "I could not announce to server: {server.id}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/converters.py:25
|
||||||
|
msgid "The Admin cog is not loaded."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: redbot/cogs/admin/converters.py:34
|
||||||
|
msgid "The provided role is not a valid selfrole."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user