mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-06 09:22:31 -05:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03230b6386 | ||
|
|
4dbf2796c0 | ||
|
|
03892f5ef1 | ||
|
|
fdf3f86ab0 | ||
|
|
7b15ad5989 | ||
|
|
443f2ca556 | ||
|
|
fa692ccc0b | ||
|
|
0c3d8af8f4 | ||
|
|
3a20c11331 | ||
|
|
aa8c9c350e | ||
|
|
139329233a | ||
|
|
d79996aeea | ||
|
|
fb839084fe | ||
|
|
dea9dde637 | ||
|
|
ebc657dcc6 | ||
|
|
80506856fb | ||
|
|
93a0e45c34 | ||
|
|
3cb2b95121 | ||
|
|
a04869ab27 | ||
|
|
c69b1185d3 | ||
|
|
ad7466a026 | ||
|
|
54dad2a604 | ||
|
|
d5899fae83 | ||
|
|
5d44bfabed | ||
|
|
b6c8be5f43 | ||
|
|
b2abfc5710 | ||
|
|
a9b328ff3c | ||
|
|
0870403299 | ||
|
|
f07b78bd0d | ||
|
|
b2497386bb | ||
|
|
f8558b98c1 | ||
|
|
84ac5f3952 | ||
|
|
404800c556 | ||
|
|
415385b747 | ||
|
|
f7dbaca340 | ||
|
|
32b4c6ce86 | ||
|
|
a3c36d4bde | ||
|
|
fc4703ef80 | ||
|
|
a301b2c758 | ||
|
|
e27682abd3 | ||
|
|
df922a0e3e | ||
|
|
51c83d958c | ||
|
|
17139ce439 | ||
|
|
61652a0306 | ||
|
|
113b97b9c9 | ||
|
|
2784730f7f | ||
|
|
1a9216b522 | ||
|
|
08fc732b7b | ||
|
|
54baf687ec |
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
@@ -53,7 +53,7 @@ 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
|
- Python 3.6.2 or greater (3.6.6 or greater on Windows)
|
||||||
- git
|
- git
|
||||||
- pip
|
- pip
|
||||||
- pipenv
|
- pipenv
|
||||||
@@ -92,7 +92,7 @@ Your PR will not be merged until all of these tests pass.
|
|||||||
### 4.3 Style
|
### 4.3 Style
|
||||||
Our style checker of choice, [black](https://github.com/ambv/black), actually happens to be an auto-formatter. The checking functionality simply detects whether or not it would try to reformat something in your code, should you run the formatter on it. For this reason, we recommend using this tool as a formatter, regardless of any disagreements you might have with the style it enforces.
|
Our style checker of choice, [black](https://github.com/ambv/black), actually happens to be an auto-formatter. The checking functionality simply detects whether or not it would try to reformat something in your code, should you run the formatter on it. For this reason, we recommend using this tool as a formatter, regardless of any disagreements you might have with the style it enforces.
|
||||||
|
|
||||||
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 <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 two things with them:
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- python: 3.6.6
|
- python: 3.6.6
|
||||||
env: TOXENV=style
|
env: TOXENV=style
|
||||||
|
|
||||||
# These jobs only occur on tag creation for V3/develop 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.6.6
|
||||||
@@ -42,7 +42,6 @@ jobs:
|
|||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
on:
|
on:
|
||||||
repo: Cog-Creators/Red-DiscordBot
|
repo: Cog-Creators/Red-DiscordBot
|
||||||
branch: V3/develop
|
|
||||||
python: 3.6.6
|
python: 3.6.6
|
||||||
tags: true
|
tags: true
|
||||||
- stage: Crowdin Deployment
|
- stage: Crowdin Deployment
|
||||||
@@ -62,6 +61,5 @@ jobs:
|
|||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
on:
|
on:
|
||||||
repo: Cog-Creators/Red-DiscordBot
|
repo: Cog-Creators/Red-DiscordBot
|
||||||
branch: V3/develop
|
|
||||||
python: 3.6.6
|
python: 3.6.6
|
||||||
tags: true
|
tags: true
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -1,7 +1,7 @@
|
|||||||
reformat:
|
reformat:
|
||||||
black -l 99 `git ls-files "*.py"`
|
black -l 99 -N `git ls-files "*.py"`
|
||||||
stylecheck:
|
stylecheck:
|
||||||
black --check -l 99 `git ls-files "*.py"`
|
black --check -l 99 -N `git ls-files "*.py"`
|
||||||
gettext:
|
gettext:
|
||||||
redgettext --command-docstrings --verbose --recursive redbot --exclude-files "redbot/pytest/**/*"
|
redgettext --command-docstrings --verbose --recursive redbot --exclude-files "redbot/pytest/**/*"
|
||||||
crowdin upload
|
crowdin upload
|
||||||
|
|||||||
280
Pipfile.lock
generated
280
Pipfile.lock
generated
@@ -16,30 +16,37 @@
|
|||||||
"default": {
|
"default": {
|
||||||
"aiohttp": {
|
"aiohttp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1a112a1fdf3802b7f2b182e22e51d71e4a8fa7387d0d38e79a268921b869e384",
|
"sha256:0419705a36b43c0ac6f15469f9c2a08cad5c939d78bd12a5c23ea167c8253b2b",
|
||||||
"sha256:33aa7c937ebaf063a860cbb0c263a771b33333a84965c6148eeafe64fb4e29ca",
|
"sha256:1812fc4bc6ac1bde007daa05d2d0f61199324e0cc893b11523e646595047ca08",
|
||||||
"sha256:550b4a0788500f6d00f41b7fdd9fcce6d78f99706a7b2f6f81d4d331c7ca468e",
|
"sha256:2214b5c0153f45256d5d52d1e0cafe53f9905ed035a142191727a5fb620c03dd",
|
||||||
"sha256:601e8e83123b4d423a9dfddf7d6943f4f520651a78ffcd50c99d065136c7ff7b",
|
"sha256:275909137f0c92c61ba6bb1af856a522d5546f1de8ea01e4e726321c697754ac",
|
||||||
"sha256:620f19ba7628b70b177f5c2e6a55a6fd6e7c8591cde38c3f8f52551733d31b66",
|
"sha256:3983611922b561868428ea1e7269e757803713f55b53502423decc509fef1650",
|
||||||
"sha256:70d56c784da1239c89d39fefa166fd429306dada641178389be4184a9c04e501",
|
"sha256:51afec6ffa50a9da4cdef188971a802beb1ca8e8edb40fa429e5e529db3475fa",
|
||||||
"sha256:7de2c9e445a5d257935011268202338538abef1aaff341a4733eca56419ca6f6",
|
"sha256:589f2ec8a101a0f340453ee6945bdfea8e1cd84c8d88e5be08716c34c0799d95",
|
||||||
"sha256:96bb80b659cc2bafa160f3f0c346ce7fc10de1ffec4908d7f9690797f155f658",
|
"sha256:789820ddc65e1f5e71516adaca2e9022498fa5a837c79ba9c692a9f8f916c330",
|
||||||
"sha256:ae7501cc6a6c37b8d4774bf2218c37be47fe42019a2570e8510fc2044e59d573",
|
"sha256:7a968a0bdaaf9abacc260911775611c9a602214a23aeb846f2eb2eeaa350c4dc",
|
||||||
"sha256:c833aa6f4c9ac3e3eb843e3d999bae51339ad33a937303f43ce78064e61cb4b6",
|
"sha256:7aeefbed253f59ea39e70c5848de42ed85cb941165357fc7e87ab5d8f1f9592b",
|
||||||
"sha256:dd81d85a342edf3d2a388e2f24d9facebc9c04550043888f970ee2f228c93059",
|
"sha256:7b2eb55c66512405103485bd7d285a839d53e7fdc261ab20e5bcc51d7aaff5de",
|
||||||
"sha256:f20deec7a3fbaec7b5eb7ad99878427ad2ee4cc16a46732b705e8121cbb3cc12",
|
"sha256:87bc95d3d333bb689c8d755b4a9d7095a2356108002149523dfc8e607d5d32a4",
|
||||||
"sha256:f52e7287eb9286a1e91e4c67c207c2573147fbaddc68f70efb5aeee5d1992f2e",
|
"sha256:9d80e40db208e29168d3723d1440ecbb06054d349c5ece6a2c5a611490830dd7",
|
||||||
"sha256:fe7b2972ff7e779e812f974aa5695edc328ecf559ceeea887ac46f06f090ad4c",
|
"sha256:a1b442195c2a77d33e4dbee67c9877ccbdd3a1f686f91eb479a9577ed8cc326b",
|
||||||
"sha256:ff1447c84a02b9cd5dd3a9332d1fb181a4386c3625765bb5caf1cfbc210ab3f9"
|
"sha256:ab3d769413b322d6092f169f316f7b21cd261a7589f7e31db779d5731b0480d8",
|
||||||
|
"sha256:b066d3dec5d0f5aee6e34e5765095dc3d6d78ef9839640141a2b20816a0642bd",
|
||||||
|
"sha256:b24e7845ae8de3e388ef4bcfcf7f96b05f52c8e633b33cf8003a6b1d726fc7c2",
|
||||||
|
"sha256:c59a953c3f8524a7c86eaeaef5bf702555be12f5668f6384149fe4bb75c52698",
|
||||||
|
"sha256:cf2cc6c2c10d242790412bea7ccf73726a9a44b4c4b073d2699ef3b48971fd95",
|
||||||
|
"sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6",
|
||||||
|
"sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0",
|
||||||
|
"sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07"
|
||||||
],
|
],
|
||||||
"version": "==3.3.2"
|
"version": "==3.4.4"
|
||||||
},
|
},
|
||||||
"aiohttp-json-rpc": {
|
"aiohttp-json-rpc": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:970806a3b9887c389095d2bde84e2b540fefeddd0bae0efcae03c65f092ce00e",
|
"sha256:00d72f40edfc7271578d545a8c47874c0e23cc5d3201ed8128481f6a4af47e32",
|
||||||
"sha256:d6f365067676e6089ac043ad31bcbabbf33d0343c42b57c36751a562fbe64fb6"
|
"sha256:02d83b6998f8a0b7e59b46f0cb8a96b475bbf82600b1f9527df47135353f1ca8"
|
||||||
],
|
],
|
||||||
"version": "==0.11.1"
|
"version": "==0.11.2"
|
||||||
},
|
},
|
||||||
"appdirs": {
|
"appdirs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -80,7 +87,7 @@
|
|||||||
"discord.py": {
|
"discord.py": {
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"git": "git://github.com/Rapptz/discord.py",
|
"git": "git://github.com/Rapptz/discord.py",
|
||||||
"ref": "00a659c6526b2445162b52eaf970adbd22c6d35d"
|
"ref": "836ae730401ea370aa10127bb9c86854c8b516ac"
|
||||||
},
|
},
|
||||||
"distro": {
|
"distro": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -89,6 +96,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==1.3.0"
|
"version": "==1.3.0"
|
||||||
},
|
},
|
||||||
|
"dnspython": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:40f563e1f7a7b80dc5a4e76ad75c23da53d62f1e15e6e517293b04e1f84ead7c",
|
||||||
|
"sha256:861e6e58faa730f9845aaaa9c6c832851fbf89382ac52915a51f89c71accdd31"
|
||||||
|
],
|
||||||
|
"version": "==1.15.0"
|
||||||
|
},
|
||||||
"e1839a8": {
|
"e1839a8": {
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"extras": [
|
"extras": [
|
||||||
@@ -126,37 +140,37 @@
|
|||||||
},
|
},
|
||||||
"multidict": {
|
"multidict": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:112eeeddd226af681dc82b756ed34aa7b6d98f9c4a15760050298c21d715473d",
|
"sha256:05eeab69bf2b0664644c62bd92fabb045163e5b8d4376a31dfb52ce0210ced7b",
|
||||||
"sha256:13b64ecb692effcabc5e29569ba9b5eb69c35112f990a16d6833ec3a9d9f8ec0",
|
"sha256:0c85880efa7cadb18e3b5eef0aa075dc9c0a3064cbbaef2e20be264b9cf47a64",
|
||||||
"sha256:1725373fb8f18c2166f8e0e5789851ccf98453c849b403945fa4ef59a16ca44e",
|
"sha256:136f5a4a6a4adeacc4dc820b8b22f0a378fb74f326e259c54d1817639d1d40a0",
|
||||||
"sha256:2061a50b7cae60a1f987503a995b2fc38e47027a937a355a124306ed9c629041",
|
"sha256:14906ad3347c7d03e9101749b16611cf2028547716d0840838d3c5e2b3b0f2d3",
|
||||||
"sha256:35b062288a9a478f627c520fd27983160fc97591017d170f966805b428d17e07",
|
"sha256:1ade4a3b71b1bf9e90c5f3d034a87fe4949c087ef1f6cd727fdd766fe8bbd121",
|
||||||
"sha256:467b134bcc227b91b8e2ef8d2931f28b50bf7eb7a04c0403d102ded22e66dbfc",
|
"sha256:22939a00a511a59f9ecc0158b8db728afef57975ce3782b3a265a319d05b9b12",
|
||||||
"sha256:475a3ece8bb450e49385414ebfae7f8fdb33f62f1ac0c12935c1cfb1b7c1076a",
|
"sha256:2b86b02d872bc5ba5b3a4530f6a7ba0b541458ab4f7c1429a12ac326231203f7",
|
||||||
"sha256:49b885287e227a24545a1126d9ac17ae43138610713dc6219b781cc0ad5c6dfc",
|
"sha256:3c11e92c3dfc321014e22fb442bc9eb70e01af30d6ce442026b0c35723448c66",
|
||||||
"sha256:4c95b2725592adb5c46642be2875c1234c32af841732c5504c17726b92082021",
|
"sha256:4ba3bd26f282b201fdbce351f1c5d17ceb224cbedb73d6e96e6ce391b354aacc",
|
||||||
"sha256:4ea7ed00f4be0f7335c9a2713a65ac3d986be789ce5ebc10821da9664cbe6b85",
|
"sha256:4c6e78d042e93751f60672989efbd6a6bc54213ed7ff695fff82784bbb9ea035",
|
||||||
"sha256:5e2d5e1d999e941b4a626aea46bdc4206877cf727107fdaa9d46a8a773a6e49b",
|
"sha256:4d80d1901b89cc935a6cf5b9fd89df66565272722fe2e5473168927a9937e0ca",
|
||||||
"sha256:8039c520ef7bb9ec7c3db3df14c570be6362f43c200ae9854d2422d4ffe175a4",
|
"sha256:4fcf71d33178a00cc34a57b29f5dab1734b9ce0f1c97fb34666deefac6f92037",
|
||||||
"sha256:81459a0ebcca09c1fcb8fe887ed13cf267d9b60fe33718fc5fd1a2a1ab49470a",
|
"sha256:52f7670b41d4b4d97866ebc38121de8bcb9813128b7c4942b07794d08193c0ab",
|
||||||
"sha256:847c3b7b9ca3268e883685dc1347a4d09f84de7bd7597310044d847590447492",
|
"sha256:5368e2b7649a26b7253c6c9e53241248aab9da49099442f5be238fde436f18c9",
|
||||||
"sha256:8551d1db45f0ca4e8ec99130767009a29a4e0dc6558a4a6808491bcd3472d325",
|
"sha256:5bb65fbb48999044938f0c0508e929b14a9b8bf4939d8263e9ea6691f7b54663",
|
||||||
"sha256:8fa7679ffe615e0c1c7b80946ab4194669be74848719adf2d7867b5e861eb073",
|
"sha256:60672bb5577472800fcca1ac9dae232d1461db9f20f055184be8ce54b0052572",
|
||||||
"sha256:a42a36f09f0f907579ff0fde547f2fde8a739a69efe4a2728835979d2bb5e17b",
|
"sha256:669e9be6d148fc0283f53e17dd140cde4dc7c87edac8319147edd5aa2a830771",
|
||||||
"sha256:a5fcad0070685c5b2d04b468bf5f4c735f5c176432f495ad055fcc4bc0a79b23",
|
"sha256:6a0b7a804e8d1716aa2c72e73210b48be83d25ba9ec5cf52cf91122285707bb1",
|
||||||
"sha256:ae22195b2a7494619b73c01129ddcddc0dfaa9e42727404b1d9a77253da3f420",
|
"sha256:79034ea3da3cf2a815e3e52afdc1f6c1894468c98bdce5d2546fa2342585497f",
|
||||||
"sha256:b360e82bdbbd862e1ce2a41cc3bbd0ab614350e813ca74801b34aac0f73465aa",
|
"sha256:79247feeef6abcc11137ad17922e865052f23447152059402fc320f99ff544bb",
|
||||||
"sha256:b96417899344c5e96bef757f4963a72d02e52653a4e0f99bbea3a531cedac59f",
|
"sha256:81671c2049e6bf42c7fd11a060f8bc58f58b7b3d6f3f951fc0b15e376a6a5a98",
|
||||||
"sha256:b9e921140b797093edfc13ac08dc2a4fd016dd711dc42bb0e1aaf180e48425a7",
|
"sha256:82ac4a5cb56cc9280d4ae52c2d2ebcd6e0668dd0f9ef17f0a9d7c82bd61e24fa",
|
||||||
"sha256:c5022b94fc330e6d177f3eb38097fb52c7df96ca0e04842c068cf0d9fc38b1e6",
|
"sha256:9436267dbbaa49dad18fbbb54f85386b0f5818d055e7b8e01d219661b6745279",
|
||||||
"sha256:cf2b117f2a8d951638efc7592fb72d3eeb2d38cc2194c26ba7f00e7190451d92",
|
"sha256:94e4140bb1343115a1afd6d84ebf8fca5fb7bfb50e1c2cbd6f2fb5d3117ef102",
|
||||||
"sha256:d79620b542d9d0e23ae9790ca2fe44f1af40ffad9936efa37bd14954bc3e2818",
|
"sha256:a2cab366eae8a0ffe0813fd8e335cf0d6b9bb6c5227315f53bb457519b811537",
|
||||||
"sha256:e2860691c11d10dac7c91bddae44f6211b3da4122d9a2ebb509c2247674d6070",
|
"sha256:a596019c3eafb1b0ae07db9f55a08578b43c79adb1fe1ab1fd818430ae59ee6f",
|
||||||
"sha256:e3a293553715afecf7e10ea02da40593f9d7f48fe48a74fc5dd3ce08a0c46188",
|
"sha256:e8848ae3cd6a784c29fae5055028bee9bffcc704d8bcad09bd46b42b44a833e2",
|
||||||
"sha256:e465be3fe7e992e5a6e16731afa6f41cb6ca53afccb4f28ea2fa6457783edf15",
|
"sha256:e8a048bfd7d5a280f27527d11449a509ddedf08b58a09a24314828631c099306",
|
||||||
"sha256:e6d27895ef922bc859d969452f247bfbe5345d9aba69b9c8dbe1ea7704f0c5d9"
|
"sha256:f6dd28a0ac60e2426a6918f36f1b4e2620fc785a0de7654cd206ba842eee57fd"
|
||||||
],
|
],
|
||||||
"version": "==4.4.0"
|
"version": "==4.4.2"
|
||||||
},
|
},
|
||||||
"pymongo": {
|
"pymongo": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -235,6 +249,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==0.1.2"
|
"version": "==0.1.2"
|
||||||
},
|
},
|
||||||
|
"schema": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687",
|
||||||
|
"sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74"
|
||||||
|
],
|
||||||
|
"version": "==0.6.8"
|
||||||
|
},
|
||||||
"websockets": {
|
"websockets": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
|
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
|
||||||
@@ -281,30 +302,37 @@
|
|||||||
"develop": {
|
"develop": {
|
||||||
"aiohttp": {
|
"aiohttp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1a112a1fdf3802b7f2b182e22e51d71e4a8fa7387d0d38e79a268921b869e384",
|
"sha256:0419705a36b43c0ac6f15469f9c2a08cad5c939d78bd12a5c23ea167c8253b2b",
|
||||||
"sha256:33aa7c937ebaf063a860cbb0c263a771b33333a84965c6148eeafe64fb4e29ca",
|
"sha256:1812fc4bc6ac1bde007daa05d2d0f61199324e0cc893b11523e646595047ca08",
|
||||||
"sha256:550b4a0788500f6d00f41b7fdd9fcce6d78f99706a7b2f6f81d4d331c7ca468e",
|
"sha256:2214b5c0153f45256d5d52d1e0cafe53f9905ed035a142191727a5fb620c03dd",
|
||||||
"sha256:601e8e83123b4d423a9dfddf7d6943f4f520651a78ffcd50c99d065136c7ff7b",
|
"sha256:275909137f0c92c61ba6bb1af856a522d5546f1de8ea01e4e726321c697754ac",
|
||||||
"sha256:620f19ba7628b70b177f5c2e6a55a6fd6e7c8591cde38c3f8f52551733d31b66",
|
"sha256:3983611922b561868428ea1e7269e757803713f55b53502423decc509fef1650",
|
||||||
"sha256:70d56c784da1239c89d39fefa166fd429306dada641178389be4184a9c04e501",
|
"sha256:51afec6ffa50a9da4cdef188971a802beb1ca8e8edb40fa429e5e529db3475fa",
|
||||||
"sha256:7de2c9e445a5d257935011268202338538abef1aaff341a4733eca56419ca6f6",
|
"sha256:589f2ec8a101a0f340453ee6945bdfea8e1cd84c8d88e5be08716c34c0799d95",
|
||||||
"sha256:96bb80b659cc2bafa160f3f0c346ce7fc10de1ffec4908d7f9690797f155f658",
|
"sha256:789820ddc65e1f5e71516adaca2e9022498fa5a837c79ba9c692a9f8f916c330",
|
||||||
"sha256:ae7501cc6a6c37b8d4774bf2218c37be47fe42019a2570e8510fc2044e59d573",
|
"sha256:7a968a0bdaaf9abacc260911775611c9a602214a23aeb846f2eb2eeaa350c4dc",
|
||||||
"sha256:c833aa6f4c9ac3e3eb843e3d999bae51339ad33a937303f43ce78064e61cb4b6",
|
"sha256:7aeefbed253f59ea39e70c5848de42ed85cb941165357fc7e87ab5d8f1f9592b",
|
||||||
"sha256:dd81d85a342edf3d2a388e2f24d9facebc9c04550043888f970ee2f228c93059",
|
"sha256:7b2eb55c66512405103485bd7d285a839d53e7fdc261ab20e5bcc51d7aaff5de",
|
||||||
"sha256:f20deec7a3fbaec7b5eb7ad99878427ad2ee4cc16a46732b705e8121cbb3cc12",
|
"sha256:87bc95d3d333bb689c8d755b4a9d7095a2356108002149523dfc8e607d5d32a4",
|
||||||
"sha256:f52e7287eb9286a1e91e4c67c207c2573147fbaddc68f70efb5aeee5d1992f2e",
|
"sha256:9d80e40db208e29168d3723d1440ecbb06054d349c5ece6a2c5a611490830dd7",
|
||||||
"sha256:fe7b2972ff7e779e812f974aa5695edc328ecf559ceeea887ac46f06f090ad4c",
|
"sha256:a1b442195c2a77d33e4dbee67c9877ccbdd3a1f686f91eb479a9577ed8cc326b",
|
||||||
"sha256:ff1447c84a02b9cd5dd3a9332d1fb181a4386c3625765bb5caf1cfbc210ab3f9"
|
"sha256:ab3d769413b322d6092f169f316f7b21cd261a7589f7e31db779d5731b0480d8",
|
||||||
|
"sha256:b066d3dec5d0f5aee6e34e5765095dc3d6d78ef9839640141a2b20816a0642bd",
|
||||||
|
"sha256:b24e7845ae8de3e388ef4bcfcf7f96b05f52c8e633b33cf8003a6b1d726fc7c2",
|
||||||
|
"sha256:c59a953c3f8524a7c86eaeaef5bf702555be12f5668f6384149fe4bb75c52698",
|
||||||
|
"sha256:cf2cc6c2c10d242790412bea7ccf73726a9a44b4c4b073d2699ef3b48971fd95",
|
||||||
|
"sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6",
|
||||||
|
"sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0",
|
||||||
|
"sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07"
|
||||||
],
|
],
|
||||||
"version": "==3.3.2"
|
"version": "==3.4.4"
|
||||||
},
|
},
|
||||||
"aiohttp-json-rpc": {
|
"aiohttp-json-rpc": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:970806a3b9887c389095d2bde84e2b540fefeddd0bae0efcae03c65f092ce00e",
|
"sha256:00d72f40edfc7271578d545a8c47874c0e23cc5d3201ed8128481f6a4af47e32",
|
||||||
"sha256:d6f365067676e6089ac043ad31bcbabbf33d0343c42b57c36751a562fbe64fb6"
|
"sha256:02d83b6998f8a0b7e59b46f0cb8a96b475bbf82600b1f9527df47135353f1ca8"
|
||||||
],
|
],
|
||||||
"version": "==0.11.1"
|
"version": "==0.11.2"
|
||||||
},
|
},
|
||||||
"alabaster": {
|
"alabaster": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -352,10 +380,10 @@
|
|||||||
},
|
},
|
||||||
"black": {
|
"black": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:22158b89c1a6b4eb333a1e65e791a3f8b998cf3b11ae094adb2570f31f769a44",
|
"sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739",
|
||||||
"sha256:4b475bbd528acce094c503a3d2dbc2d05a4075f6d0ef7d9e7514518e14cc5191"
|
"sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"
|
||||||
],
|
],
|
||||||
"version": "==18.6b4"
|
"version": "==18.9b0"
|
||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -373,10 +401,10 @@
|
|||||||
},
|
},
|
||||||
"click": {
|
"click": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
|
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||||
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
|
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||||
],
|
],
|
||||||
"version": "==6.7"
|
"version": "==7.0"
|
||||||
},
|
},
|
||||||
"colorama": {
|
"colorama": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -460,44 +488,44 @@
|
|||||||
},
|
},
|
||||||
"multidict": {
|
"multidict": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:112eeeddd226af681dc82b756ed34aa7b6d98f9c4a15760050298c21d715473d",
|
"sha256:05eeab69bf2b0664644c62bd92fabb045163e5b8d4376a31dfb52ce0210ced7b",
|
||||||
"sha256:13b64ecb692effcabc5e29569ba9b5eb69c35112f990a16d6833ec3a9d9f8ec0",
|
"sha256:0c85880efa7cadb18e3b5eef0aa075dc9c0a3064cbbaef2e20be264b9cf47a64",
|
||||||
"sha256:1725373fb8f18c2166f8e0e5789851ccf98453c849b403945fa4ef59a16ca44e",
|
"sha256:136f5a4a6a4adeacc4dc820b8b22f0a378fb74f326e259c54d1817639d1d40a0",
|
||||||
"sha256:2061a50b7cae60a1f987503a995b2fc38e47027a937a355a124306ed9c629041",
|
"sha256:14906ad3347c7d03e9101749b16611cf2028547716d0840838d3c5e2b3b0f2d3",
|
||||||
"sha256:35b062288a9a478f627c520fd27983160fc97591017d170f966805b428d17e07",
|
"sha256:1ade4a3b71b1bf9e90c5f3d034a87fe4949c087ef1f6cd727fdd766fe8bbd121",
|
||||||
"sha256:467b134bcc227b91b8e2ef8d2931f28b50bf7eb7a04c0403d102ded22e66dbfc",
|
"sha256:22939a00a511a59f9ecc0158b8db728afef57975ce3782b3a265a319d05b9b12",
|
||||||
"sha256:475a3ece8bb450e49385414ebfae7f8fdb33f62f1ac0c12935c1cfb1b7c1076a",
|
"sha256:2b86b02d872bc5ba5b3a4530f6a7ba0b541458ab4f7c1429a12ac326231203f7",
|
||||||
"sha256:49b885287e227a24545a1126d9ac17ae43138610713dc6219b781cc0ad5c6dfc",
|
"sha256:3c11e92c3dfc321014e22fb442bc9eb70e01af30d6ce442026b0c35723448c66",
|
||||||
"sha256:4c95b2725592adb5c46642be2875c1234c32af841732c5504c17726b92082021",
|
"sha256:4ba3bd26f282b201fdbce351f1c5d17ceb224cbedb73d6e96e6ce391b354aacc",
|
||||||
"sha256:4ea7ed00f4be0f7335c9a2713a65ac3d986be789ce5ebc10821da9664cbe6b85",
|
"sha256:4c6e78d042e93751f60672989efbd6a6bc54213ed7ff695fff82784bbb9ea035",
|
||||||
"sha256:5e2d5e1d999e941b4a626aea46bdc4206877cf727107fdaa9d46a8a773a6e49b",
|
"sha256:4d80d1901b89cc935a6cf5b9fd89df66565272722fe2e5473168927a9937e0ca",
|
||||||
"sha256:8039c520ef7bb9ec7c3db3df14c570be6362f43c200ae9854d2422d4ffe175a4",
|
"sha256:4fcf71d33178a00cc34a57b29f5dab1734b9ce0f1c97fb34666deefac6f92037",
|
||||||
"sha256:81459a0ebcca09c1fcb8fe887ed13cf267d9b60fe33718fc5fd1a2a1ab49470a",
|
"sha256:52f7670b41d4b4d97866ebc38121de8bcb9813128b7c4942b07794d08193c0ab",
|
||||||
"sha256:847c3b7b9ca3268e883685dc1347a4d09f84de7bd7597310044d847590447492",
|
"sha256:5368e2b7649a26b7253c6c9e53241248aab9da49099442f5be238fde436f18c9",
|
||||||
"sha256:8551d1db45f0ca4e8ec99130767009a29a4e0dc6558a4a6808491bcd3472d325",
|
"sha256:5bb65fbb48999044938f0c0508e929b14a9b8bf4939d8263e9ea6691f7b54663",
|
||||||
"sha256:8fa7679ffe615e0c1c7b80946ab4194669be74848719adf2d7867b5e861eb073",
|
"sha256:60672bb5577472800fcca1ac9dae232d1461db9f20f055184be8ce54b0052572",
|
||||||
"sha256:a42a36f09f0f907579ff0fde547f2fde8a739a69efe4a2728835979d2bb5e17b",
|
"sha256:669e9be6d148fc0283f53e17dd140cde4dc7c87edac8319147edd5aa2a830771",
|
||||||
"sha256:a5fcad0070685c5b2d04b468bf5f4c735f5c176432f495ad055fcc4bc0a79b23",
|
"sha256:6a0b7a804e8d1716aa2c72e73210b48be83d25ba9ec5cf52cf91122285707bb1",
|
||||||
"sha256:ae22195b2a7494619b73c01129ddcddc0dfaa9e42727404b1d9a77253da3f420",
|
"sha256:79034ea3da3cf2a815e3e52afdc1f6c1894468c98bdce5d2546fa2342585497f",
|
||||||
"sha256:b360e82bdbbd862e1ce2a41cc3bbd0ab614350e813ca74801b34aac0f73465aa",
|
"sha256:79247feeef6abcc11137ad17922e865052f23447152059402fc320f99ff544bb",
|
||||||
"sha256:b96417899344c5e96bef757f4963a72d02e52653a4e0f99bbea3a531cedac59f",
|
"sha256:81671c2049e6bf42c7fd11a060f8bc58f58b7b3d6f3f951fc0b15e376a6a5a98",
|
||||||
"sha256:b9e921140b797093edfc13ac08dc2a4fd016dd711dc42bb0e1aaf180e48425a7",
|
"sha256:82ac4a5cb56cc9280d4ae52c2d2ebcd6e0668dd0f9ef17f0a9d7c82bd61e24fa",
|
||||||
"sha256:c5022b94fc330e6d177f3eb38097fb52c7df96ca0e04842c068cf0d9fc38b1e6",
|
"sha256:9436267dbbaa49dad18fbbb54f85386b0f5818d055e7b8e01d219661b6745279",
|
||||||
"sha256:cf2b117f2a8d951638efc7592fb72d3eeb2d38cc2194c26ba7f00e7190451d92",
|
"sha256:94e4140bb1343115a1afd6d84ebf8fca5fb7bfb50e1c2cbd6f2fb5d3117ef102",
|
||||||
"sha256:d79620b542d9d0e23ae9790ca2fe44f1af40ffad9936efa37bd14954bc3e2818",
|
"sha256:a2cab366eae8a0ffe0813fd8e335cf0d6b9bb6c5227315f53bb457519b811537",
|
||||||
"sha256:e2860691c11d10dac7c91bddae44f6211b3da4122d9a2ebb509c2247674d6070",
|
"sha256:a596019c3eafb1b0ae07db9f55a08578b43c79adb1fe1ab1fd818430ae59ee6f",
|
||||||
"sha256:e3a293553715afecf7e10ea02da40593f9d7f48fe48a74fc5dd3ce08a0c46188",
|
"sha256:e8848ae3cd6a784c29fae5055028bee9bffcc704d8bcad09bd46b42b44a833e2",
|
||||||
"sha256:e465be3fe7e992e5a6e16731afa6f41cb6ca53afccb4f28ea2fa6457783edf15",
|
"sha256:e8a048bfd7d5a280f27527d11449a509ddedf08b58a09a24314828631c099306",
|
||||||
"sha256:e6d27895ef922bc859d969452f247bfbe5345d9aba69b9c8dbe1ea7704f0c5d9"
|
"sha256:f6dd28a0ac60e2426a6918f36f1b4e2620fc785a0de7654cd206ba842eee57fd"
|
||||||
],
|
],
|
||||||
"version": "==4.4.0"
|
"version": "==4.4.2"
|
||||||
},
|
},
|
||||||
"packaging": {
|
"packaging": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0",
|
"sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
|
||||||
"sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b"
|
"sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9"
|
||||||
],
|
],
|
||||||
"version": "==17.1"
|
"version": "==18.0"
|
||||||
},
|
},
|
||||||
"pluggy": {
|
"pluggy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -524,17 +552,17 @@
|
|||||||
},
|
},
|
||||||
"pyparsing": {
|
"pyparsing": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04",
|
"sha256:bc6c7146b91af3f567cf6daeaec360bc07d45ffec4cf5353f4d7a208ce7ca30a",
|
||||||
"sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010"
|
"sha256:d29593d8ebe7b57d6967b62494f8c72b03ac0262b1eed63826c6f788b3606401"
|
||||||
],
|
],
|
||||||
"version": "==2.2.0"
|
"version": "==2.2.2"
|
||||||
},
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2d7c49e931316cc7d1638a3e5f54f5d7b4e5225972b3c9838f3584788d27f349",
|
"sha256:7e258ee50338f4e46957f9e09a0f10fb1c2d05493fa901d113a8dafd0790de4e",
|
||||||
"sha256:ad0c7db7b5d4081631e0155f5c61b80ad76ce148551aaafe3a718d65a7508b18"
|
"sha256:9332147e9af2dcf46cd7ceb14d5acadb6564744ddff1fe8c17f0ce60ece7d9a2"
|
||||||
],
|
],
|
||||||
"version": "==3.7.4"
|
"version": "==3.8.2"
|
||||||
},
|
},
|
||||||
"pytest-asyncio": {
|
"pytest-asyncio": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -594,6 +622,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.19.1"
|
"version": "==2.19.1"
|
||||||
},
|
},
|
||||||
|
"schema": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687",
|
||||||
|
"sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74"
|
||||||
|
],
|
||||||
|
"version": "==0.6.8"
|
||||||
|
},
|
||||||
"six": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||||
@@ -610,10 +645,10 @@
|
|||||||
},
|
},
|
||||||
"sphinx": {
|
"sphinx": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:a07050845cc9a2f4026a6035cc8ed795a5ce7be6528bbc82032385c10807dfe7",
|
"sha256:217a7705adcb573da5bbe1e0f5cab4fa0bd89fd9342c9159121746f593c2d5a4",
|
||||||
"sha256:d719de667218d763e8fd144b7fcfeefd8d434a6201f76bf9f0f0c1fa6f47fcdb"
|
"sha256:a602513f385f1d5785ff1ca420d9c7eb1a1b63381733b2f0ea8188a391314a86"
|
||||||
],
|
],
|
||||||
"version": "==1.7.8"
|
"version": "==1.7.9"
|
||||||
},
|
},
|
||||||
"sphinx-rtd-theme": {
|
"sphinx-rtd-theme": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -638,17 +673,18 @@
|
|||||||
},
|
},
|
||||||
"toml": {
|
"toml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d"
|
"sha256:380178cde50a6a79f9d2cf6f42a62a5174febe5eea4126fe4038785f1d888d42",
|
||||||
|
"sha256:a7901919d3e4f92ffba7ff40a9d697e35bbbc8a8049fe8da742f34c83606d957"
|
||||||
],
|
],
|
||||||
"version": "==0.9.4"
|
"version": "==0.9.6"
|
||||||
},
|
},
|
||||||
"tox": {
|
"tox": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:37cf240781b662fb790710c6998527e65ca6851eace84d1595ee71f7af4e85f7",
|
"sha256:7f802b37fffd3b5ef2aab104943fa5dad24bf9564bb7e732e54b8d0cfec2fca0",
|
||||||
"sha256:eb61aa5bcce65325538686f09848f04ef679b5cd9b83cc491272099b28739600"
|
"sha256:cc97859bd7f38aa5b3b8ba55ffe7ee9952e7050faad1aedc0829cd3db2fb61d6"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.2.1"
|
"version": "==3.4.0"
|
||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
https://github.com/Rapptz/discord.py/tarball/00a659c6526b2445162b52eaf970adbd22c6d35d#egg=discord.py-1.0.0a0
|
https://github.com/Rapptz/discord.py/tarball/836ae730401ea370aa10127bb9c86854c8b516ac#egg=discord.py-1.0.0a0
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ CustomCommands allows you to create simple commands for your bot without requiri
|
|||||||
|
|
||||||
If the command you attempt to create shares a name with an already loaded command, you cannot overwrite it with this cog.
|
If the command you attempt to create shares a name with an already loaded command, you cannot overwrite it with this cog.
|
||||||
|
|
||||||
|
---------
|
||||||
|
Cooldowns
|
||||||
|
---------
|
||||||
|
|
||||||
|
You can set cooldowns for your custom commands. If a command is on cooldown, it will not be triggered.
|
||||||
|
|
||||||
|
You can set cooldowns per member or per channel, or set a cooldown guild-wide. You can also set multiple types of cooldown on a single custom command. All cooldowns must pass before the command will trigger.
|
||||||
|
|
||||||
------------------
|
------------------
|
||||||
Context Parameters
|
Context Parameters
|
||||||
------------------
|
------------------
|
||||||
|
|||||||
@@ -8,100 +8,90 @@ Permissions Cog Reference
|
|||||||
How it works
|
How it works
|
||||||
------------
|
------------
|
||||||
|
|
||||||
When loaded, the permissions cog will allow you
|
When loaded, the permissions cog will allow you to define extra custom rules for who can use a
|
||||||
to define extra custom rules for who can use a command
|
command.
|
||||||
|
|
||||||
If no applicable rules are found, the command will behave as if
|
If no applicable rules are found, the command will behave normally.
|
||||||
the cog was not loaded.
|
|
||||||
|
Rules can also be added to cogs, which will affect all commands from that cog. The cog name can be
|
||||||
|
found from the help menu.
|
||||||
|
|
||||||
-------------
|
-------------
|
||||||
Rule priority
|
Rule priority
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
Rules set will be checked in the following order
|
Rules set for subcommands will take precedence over rules set for the parent commands, which
|
||||||
|
lastly take precedence over rules set for the cog. So for example, if a user is denied the Core
|
||||||
|
cog, but allowed the ``[p]set token`` command, the user will not be able to use any command in the
|
||||||
|
Core cog except for ``[p]set token``.
|
||||||
|
|
||||||
|
In terms of scope, global rules will be checked first, then server rules.
|
||||||
|
|
||||||
1. Owner level command specific settings
|
For each of those, the first rule pertaining to one of the following models will be used:
|
||||||
2. Owner level cog specific settings
|
|
||||||
3. Server level command specific settings
|
|
||||||
4. Server level cog specific settings
|
|
||||||
|
|
||||||
For each of those, settings have varying priorities (listed below, highest to lowest priority)
|
1. User
|
||||||
|
2. Voice channel
|
||||||
|
3. Text channel
|
||||||
|
4. Channel category
|
||||||
|
5. Roles, highest to lowest
|
||||||
|
6. Server (can only be in global rules)
|
||||||
|
7. Default rules
|
||||||
|
|
||||||
1. User whitelist
|
In private messages, only global rules about a user will be checked.
|
||||||
2. User blacklist
|
|
||||||
3. Voice Channel whitelist
|
|
||||||
4. Voice Channel blacklist
|
|
||||||
5. Text Channel whitelist
|
|
||||||
6. Text Channel blacklist
|
|
||||||
7. Role settings (see below)
|
|
||||||
8. Server whitelist
|
|
||||||
9. Server blacklist
|
|
||||||
10. Default settings
|
|
||||||
|
|
||||||
For the role whitelist and blacklist settings,
|
|
||||||
roles will be checked individually in order from highest to lowest role the user has
|
|
||||||
Each role will be checked for whitelist, then blacklist. The first role with a setting
|
|
||||||
found will be the one used.
|
|
||||||
|
|
||||||
-------------------------
|
-------------------------
|
||||||
Setting Rules from a file
|
Setting Rules From a File
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
The permissions cog can set rules from a yaml file:
|
The permissions cog can also set, display or update rules with a YAML file with the
|
||||||
All entries are based on ID.
|
``[p]permissions yaml`` command. Models must be represented by ID. Rules must be ``true`` for
|
||||||
An example of the expected format is shown below.
|
allow, or ``false`` for deny. Here is an example:
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
cogs:
|
COG:
|
||||||
Admin:
|
Admin:
|
||||||
allow:
|
78631113035100160: true
|
||||||
- 78631113035100160
|
96733288462286848: false
|
||||||
deny:
|
|
||||||
- 96733288462286848
|
|
||||||
Audio:
|
Audio:
|
||||||
allow:
|
133049272517001216: true
|
||||||
- 133049272517001216
|
default: false
|
||||||
default: deny
|
COMMAND:
|
||||||
commands:
|
|
||||||
cleanup bot:
|
cleanup bot:
|
||||||
allow:
|
78631113035100160: true
|
||||||
- 78631113035100160
|
default: false
|
||||||
default: deny
|
|
||||||
ping:
|
ping:
|
||||||
deny:
|
96733288462286848: false
|
||||||
- 96733288462286848
|
default: true
|
||||||
default: allow
|
|
||||||
|
|
||||||
----------------------
|
----------------------
|
||||||
Example configurations
|
Example configurations
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
Locking Audio cog to approved server(s) as a bot owner
|
Locking the ``[p]play`` command to approved server(s) as a bot owner:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
[p]permissions setglobaldefault Audio deny
|
[p]permissions setglobaldefault play deny
|
||||||
[p]permissions addglobalrule allow Audio [server ID or name]
|
[p]permissions addglobalrule allow play [server ID or name]
|
||||||
|
|
||||||
Locking Audio 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 setguilddefault deny play
|
[p]permissions setserverdefault deny play
|
||||||
[p]permissions setguilddefault deny "playlist start"
|
[p]permissions setserverdefault deny "playlist start"
|
||||||
[p]permissions addguildrule allow play [voice channel ID or name]
|
[p]permissions addserverrule allow play [voice channel ID or name]
|
||||||
[p]permissions addguildrule allow "playlist start" [voice channel ID or name]
|
[p]permissions addserverrule allow "playlist start" [voice channel ID or name]
|
||||||
|
|
||||||
Allowing extra roles to use cleanup
|
Allowing extra roles to use ``[p]cleanup``:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
[p]permissions addguildrule allow Cleanup [role ID]
|
[p]permissions addserverrule allow cleanup [role ID]
|
||||||
|
|
||||||
Preventing cleanup from being used in channels where message history is important:
|
Preventing ``[p]cleanup`` from being used in channels where message history is important:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
[p]permissions addguildrule deny Cleanup [channel ID or mention]
|
[p]permissions addserverrule deny cleanup [channel ID or mention]
|
||||||
|
|||||||
10
docs/conf.py
10
docs/conf.py
@@ -39,6 +39,7 @@ extensions = [
|
|||||||
"sphinx.ext.intersphinx",
|
"sphinx.ext.intersphinx",
|
||||||
"sphinx.ext.viewcode",
|
"sphinx.ext.viewcode",
|
||||||
"sphinx.ext.napoleon",
|
"sphinx.ext.napoleon",
|
||||||
|
"sphinx.ext.doctest",
|
||||||
"sphinxcontrib.asyncio",
|
"sphinxcontrib.asyncio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -197,9 +198,16 @@ texinfo_documents = [
|
|||||||
linkcheck_ignore = [r"https://java.com*"]
|
linkcheck_ignore = [r"https://java.com*"]
|
||||||
|
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# -- Options for extensions -----------------------------------------------
|
||||||
|
|
||||||
|
# Intersphinx
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
"python": ("https://docs.python.org/3.6", None),
|
"python": ("https://docs.python.org/3.6", None),
|
||||||
"dpy": ("https://discordpy.readthedocs.io/en/rewrite/", None),
|
"dpy": ("https://discordpy.readthedocs.io/en/rewrite/", None),
|
||||||
"motor": ("https://motor.readthedocs.io/en/stable/", None),
|
"motor": ("https://motor.readthedocs.io/en/stable/", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Doctest
|
||||||
|
# If this string is non-empty, all blocks with ``>>>`` in them will be
|
||||||
|
# tested, not just the ones explicitly marked with ``.. doctest::``
|
||||||
|
doctest_test_doctest_blocks = ""
|
||||||
|
|||||||
11
docs/framework_checks.rst
Normal file
11
docs/framework_checks.rst
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.. _checks:
|
||||||
|
|
||||||
|
========================
|
||||||
|
Command Check Decorators
|
||||||
|
========================
|
||||||
|
|
||||||
|
The following are all decorators for commands, which add restrictions to where and when they can be
|
||||||
|
run.
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.checks
|
||||||
|
:members:
|
||||||
@@ -21,3 +21,6 @@ extend functionlities used throughout the bot, as outlined below.
|
|||||||
|
|
||||||
.. autoclass:: redbot.core.commands.Context
|
.. autoclass:: redbot.core.commands.Context
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.commands.requires
|
||||||
|
:members: PrivilegeLevel, PermState, Requires
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ This usage guide will cover the following features:
|
|||||||
|
|
||||||
- :py:meth:`Group.get_raw`
|
- :py:meth:`Group.get_raw`
|
||||||
- :py:meth:`Group.set_raw`
|
- :py:meth:`Group.set_raw`
|
||||||
|
- :py:meth:`Group.clear_raw`
|
||||||
|
|
||||||
For this example let's suppose that we're creating a cog that allows users to buy and own multiple pets using
|
For this example let's suppose that we're creating a cog that allows users to buy and own multiple pets using
|
||||||
the built-in Economy credits::
|
the built-in Economy credits::
|
||||||
@@ -290,6 +291,37 @@ We're responsible pet owners here, so we've also got to have a way to feed our p
|
|||||||
|
|
||||||
await ctx.send("Your pet is now at {}/100 hunger!".format(new_hunger)
|
await ctx.send("Your pet is now at {}/100 hunger!".format(new_hunger)
|
||||||
|
|
||||||
|
Of course, if we're less than responsible pet owners, there are consequences::
|
||||||
|
|
||||||
|
#continued
|
||||||
|
@commands.command()
|
||||||
|
async def adopt(self, ctx, pet_name: str, *, member: discord.Member):
|
||||||
|
try:
|
||||||
|
pet = await self.conf.user(member).pets.get_raw(pet_name)
|
||||||
|
except KeyError:
|
||||||
|
await ctx.send("That person doesn't own that pet!")
|
||||||
|
return
|
||||||
|
|
||||||
|
hunger = pet.get("hunger")
|
||||||
|
if hunger < 80:
|
||||||
|
await ctx.send("That pet is too well taken care of to be adopted.")
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.conf.user(member).pets.clear_raw(pet_name)
|
||||||
|
|
||||||
|
# this is equivalent to doing the following
|
||||||
|
|
||||||
|
pets = await self.conf.user(member).pets()
|
||||||
|
del pets[pet_name]
|
||||||
|
await self.conf.user(member).pets.set(pets)
|
||||||
|
|
||||||
|
await self.conf.user(ctx.author).pets.set_raw(pet_name, value=pet)
|
||||||
|
await ctx.send(
|
||||||
|
"Your request to adopt this pet has been granted due to "
|
||||||
|
"how poorly it was taken care of."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
*************
|
*************
|
||||||
V2 Data Usage
|
V2 Data Usage
|
||||||
*************
|
*************
|
||||||
|
|||||||
@@ -22,12 +22,18 @@ Embed Helpers
|
|||||||
.. automodule:: redbot.core.utils.embed
|
.. automodule:: redbot.core.utils.embed
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Menu Helpers
|
Reaction Menus
|
||||||
============
|
==============
|
||||||
|
|
||||||
.. automodule:: redbot.core.utils.menus
|
.. automodule:: redbot.core.utils.menus
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
Event Predicates
|
||||||
|
================
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.utils.predicates
|
||||||
|
:members:
|
||||||
|
|
||||||
Mod Helpers
|
Mod Helpers
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ you in the process.
|
|||||||
Getting started
|
Getting started
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
To start off, be sure that you have installed Python 3.6.2 or higher. Open a terminal or command prompt and type
|
To start off, be sure that you have installed Python 3.6.2 or higher (3.6.6 or higher on Windows).
|
||||||
:code:`pip install --process-dependency-links -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 --process-dependency-links -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.
|
||||||
|
|
||||||
|
|||||||
@@ -33,14 +33,15 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
guide_data_conversion
|
guide_data_conversion
|
||||||
framework_bank
|
framework_bank
|
||||||
framework_bot
|
framework_bot
|
||||||
|
framework_checks
|
||||||
framework_cogmanager
|
framework_cogmanager
|
||||||
|
framework_commands
|
||||||
framework_config
|
framework_config
|
||||||
framework_datamanager
|
framework_datamanager
|
||||||
framework_downloader
|
framework_downloader
|
||||||
framework_events
|
framework_events
|
||||||
framework_i18n
|
framework_i18n
|
||||||
framework_modlog
|
framework_modlog
|
||||||
framework_commands
|
|
||||||
framework_rpc
|
framework_rpc
|
||||||
framework_utils
|
framework_utils
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Installing Red on Windows
|
|||||||
Needed Software
|
Needed Software
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
* `Python <https://www.python.org/downloads/>`_ - Red needs Python 3.6.2 or greater
|
* `Python <https://www.python.org/downloads/>`_ - Red needs Python 3.6.6 or greater on Windows
|
||||||
|
|
||||||
.. 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
|
||||||
|
|||||||
4
make.bat
4
make.bat
@@ -14,11 +14,11 @@ for /F "tokens=* USEBACKQ" %%A in (`git ls-files "*.py"`) do (
|
|||||||
goto %1
|
goto %1
|
||||||
|
|
||||||
:reformat
|
:reformat
|
||||||
black -l 99 !PYFILES!
|
black -l 99 -N !PYFILES!
|
||||||
exit /B %ERRORLEVEL%
|
exit /B %ERRORLEVEL%
|
||||||
|
|
||||||
:stylecheck
|
:stylecheck
|
||||||
black -l 99 --check !PYFILES!
|
black -l 99 -N --check !PYFILES!
|
||||||
exit /B %ERRORLEVEL%
|
exit /B %ERRORLEVEL%
|
||||||
|
|
||||||
:help
|
:help
|
||||||
|
|||||||
@@ -1,18 +1,34 @@
|
|||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
import discord
|
import discord
|
||||||
from colorama import init
|
import colorama
|
||||||
|
|
||||||
init()
|
|
||||||
# Let's do all the dumb version checking in one place.
|
# 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:
|
||||||
|
print(
|
||||||
|
f"Python {'.'.join(map(str, MIN_PYTHON_VERSION))} is required to run Red, but you have "
|
||||||
|
f"{sys.version}! Please update Python."
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if discord.version_info.major < 1:
|
if discord.version_info.major < 1:
|
||||||
print(
|
print(
|
||||||
"You are not running the rewritten version of discord.py.\n\n"
|
"You are not running the rewritten version of discord.py.\n\n"
|
||||||
"In order to use Red v3 you MUST be running d.py version"
|
"In order to use Red V3 you MUST be running d.py version "
|
||||||
" >= 1.0.0."
|
"1.0.0 or greater."
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
colorama.init()
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
@@ -1,45 +1,52 @@
|
|||||||
|
import logging
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from redbot.core import Config, checks, commands
|
from redbot.core import Config, checks, commands
|
||||||
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
import logging
|
|
||||||
|
|
||||||
from redbot.core.utils.chat_formatting import box
|
from redbot.core.utils.chat_formatting import box
|
||||||
from .announcer import Announcer
|
from .announcer import Announcer
|
||||||
from .converters import MemberDefaultAuthor, SelfRole
|
from .converters import MemberDefaultAuthor, SelfRole
|
||||||
|
|
||||||
log = logging.getLogger("red.admin")
|
log = logging.getLogger("red.admin")
|
||||||
|
|
||||||
GENERIC_FORBIDDEN = (
|
T_ = Translator("Admin", __file__)
|
||||||
|
|
||||||
|
_ = lambda s: s
|
||||||
|
GENERIC_FORBIDDEN = _(
|
||||||
"I attempted to do something that Discord denied me permissions for."
|
"I attempted to do something that Discord denied me permissions for."
|
||||||
" Your command failed to successfully complete."
|
" Your command failed to successfully complete."
|
||||||
)
|
)
|
||||||
|
|
||||||
HIERARCHY_ISSUE = (
|
HIERARCHY_ISSUE = _(
|
||||||
"I tried to add {role.name} to {member.display_name} but that role"
|
"I tried to add {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 add {role.name} to {member.display_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."
|
||||||
)
|
)
|
||||||
|
|
||||||
RUNNING_ANNOUNCEMENT = (
|
RUNNING_ANNOUNCEMENT = _(
|
||||||
"I am already announcing something. If you would like to make a"
|
"I am already announcing something. If you would like to make a"
|
||||||
" different announcement please use `{prefix}announce cancel`"
|
" different announcement please use `{prefix}announce cancel`"
|
||||||
" first."
|
" first."
|
||||||
)
|
)
|
||||||
|
_ = T_
|
||||||
|
|
||||||
|
|
||||||
class Admin:
|
@cog_i18n(_)
|
||||||
|
class Admin(commands.Cog):
|
||||||
|
"""A collection of server administration utilities."""
|
||||||
|
|
||||||
def __init__(self, config=Config):
|
def __init__(self, config=Config):
|
||||||
|
super().__init__()
|
||||||
self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
|
self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
|
||||||
|
|
||||||
self.conf.register_global(serverlocked=False)
|
self.conf.register_global(serverlocked=False)
|
||||||
@@ -97,13 +104,14 @@ class Admin:
|
|||||||
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, HIERARCHY_ISSUE, role=role, member=member)
|
await self.complain(ctx, T_(HIERARCHY_ISSUE), role=role, member=member)
|
||||||
else:
|
else:
|
||||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"I successfully added {role.name} to"
|
_("I successfully added {role.name} to {member.display_name}").format(
|
||||||
" {member.display_name}".format(role=role, member=member)
|
role=role, member=member
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _removerole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
|
async def _removerole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
|
||||||
@@ -111,13 +119,14 @@ class Admin:
|
|||||||
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, HIERARCHY_ISSUE, role=role, member=member)
|
await self.complain(ctx, T_(HIERARCHY_ISSUE), role=role, member=member)
|
||||||
else:
|
else:
|
||||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"I successfully removed {role.name} from"
|
_("I successfully removed {role.name} from {member.display_name}").format(
|
||||||
" {member.display_name}".format(role=role, member=member)
|
role=role, member=member
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@@ -126,8 +135,8 @@ class Admin:
|
|||||||
async def addrole(
|
async def addrole(
|
||||||
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
||||||
):
|
):
|
||||||
"""
|
"""Add a role to a user.
|
||||||
Adds a role to a user.
|
|
||||||
If user is left blank it defaults to the author of the command.
|
If user is left blank it defaults to the author of the command.
|
||||||
"""
|
"""
|
||||||
if user is None:
|
if user is None:
|
||||||
@@ -136,7 +145,7 @@ class Admin:
|
|||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self._addrole(ctx, user, rolename)
|
await self._addrole(ctx, user, rolename)
|
||||||
else:
|
else:
|
||||||
await self.complain(ctx, USER_HIERARCHY_ISSUE, member=ctx.author, role=rolename)
|
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE), member=ctx.author, role=rolename)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -144,8 +153,8 @@ class Admin:
|
|||||||
async def removerole(
|
async def removerole(
|
||||||
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
||||||
):
|
):
|
||||||
"""
|
"""Remove a role from a user.
|
||||||
Removes a role from a user.
|
|
||||||
If user is left blank it defaults to the author of the command.
|
If user is left blank it defaults to the author of the command.
|
||||||
"""
|
"""
|
||||||
if user is None:
|
if user is None:
|
||||||
@@ -154,50 +163,54 @@ class Admin:
|
|||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self._removerole(ctx, user, rolename)
|
await self._removerole(ctx, user, rolename)
|
||||||
else:
|
else:
|
||||||
await self.complain(ctx, USER_HIERARCHY_ISSUE)
|
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE))
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.admin_or_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def editrole(self, ctx: commands.Context):
|
async def editrole(self, ctx: commands.Context):
|
||||||
"""Edits roles settings"""
|
"""Edit role settings."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@editrole.command(name="colour", aliases=["color"])
|
@editrole.command(name="colour", aliases=["color"])
|
||||||
async def editrole_colour(
|
async def editrole_colour(
|
||||||
self, ctx: commands.Context, role: discord.Role, value: discord.Colour
|
self, ctx: commands.Context, role: discord.Role, value: discord.Colour
|
||||||
):
|
):
|
||||||
"""Edits a role's colour
|
"""Edit a role's colour.
|
||||||
|
|
||||||
Use double quotes if the role contains spaces.
|
Use double quotes if the role contains spaces.
|
||||||
Colour must be in hexadecimal format.
|
Colour must be in hexadecimal format.
|
||||||
\"http://www.w3schools.com/colors/colors_picker.asp\"
|
[Online colour picker](http://www.w3schools.com/colors/colors_picker.asp)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
!editrole colour \"The Transistor\" #ff0000
|
`[p]editrole colour "The Transistor" #ff0000`
|
||||||
!editrole colour Test #ff9900"""
|
`[p]editrole colour Test #ff9900`
|
||||||
|
"""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
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, USER_HIERARCHY_ISSUE)
|
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE))
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await role.edit(reason=reason, color=value)
|
await role.edit(reason=reason, color=value)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
|
||||||
else:
|
else:
|
||||||
log.info(reason)
|
log.info(reason)
|
||||||
await ctx.send("Done.")
|
await ctx.send(_("Done."))
|
||||||
|
|
||||||
@editrole.command(name="name")
|
@editrole.command(name="name")
|
||||||
@checks.admin_or_permissions(administrator=True)
|
@checks.admin_or_permissions(administrator=True)
|
||||||
async def edit_role_name(self, ctx: commands.Context, role: discord.Role, *, name: str):
|
async def edit_role_name(self, ctx: commands.Context, role: discord.Role, *, name: str):
|
||||||
"""Edits a role's name
|
"""Edit a role's name.
|
||||||
|
|
||||||
Use double quotes if the role or the name contain spaces.
|
Use double quotes if the role or the name contain spaces.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
!editrole name \"The Transistor\" Test"""
|
`[p]editrole name \"The Transistor\" Test`
|
||||||
|
"""
|
||||||
author = ctx.message.author
|
author = ctx.message.author
|
||||||
old_name = role.name
|
old_name = role.name
|
||||||
reason = "{}({}) changed the name of role '{}' to '{}'".format(
|
reason = "{}({}) changed the name of role '{}' to '{}'".format(
|
||||||
@@ -205,73 +218,74 @@ class Admin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not self.pass_user_hierarchy_check(ctx, role):
|
if not self.pass_user_hierarchy_check(ctx, role):
|
||||||
await self.complain(ctx, USER_HIERARCHY_ISSUE)
|
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE))
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await role.edit(reason=reason, name=name)
|
await role.edit(reason=reason, name=name)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
|
||||||
else:
|
else:
|
||||||
log.info(reason)
|
log.info(reason)
|
||||||
await ctx.send("Done.")
|
await ctx.send(_("Done."))
|
||||||
|
|
||||||
@commands.group(invoke_without_command=True)
|
@commands.group(invoke_without_command=True)
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def announce(self, ctx: commands.Context, *, message: str):
|
async def announce(self, ctx: commands.Context, *, message: str):
|
||||||
"""
|
"""Announce a message to all servers the bot is in."""
|
||||||
Announces a message to all servers the bot is in.
|
|
||||||
"""
|
|
||||||
if not self.is_announcing():
|
if not self.is_announcing():
|
||||||
announcer = Announcer(ctx, message, config=self.conf)
|
announcer = Announcer(ctx, message, config=self.conf)
|
||||||
announcer.start()
|
announcer.start()
|
||||||
|
|
||||||
self.__current_announcer = announcer
|
self.__current_announcer = announcer
|
||||||
|
|
||||||
await ctx.send("The announcement has begun.")
|
await ctx.send(_("The announcement has begun."))
|
||||||
else:
|
else:
|
||||||
prefix = ctx.prefix
|
prefix = ctx.prefix
|
||||||
await self.complain(ctx, RUNNING_ANNOUNCEMENT, prefix=prefix)
|
await self.complain(ctx, T_(RUNNING_ANNOUNCEMENT), prefix=prefix)
|
||||||
|
|
||||||
@announce.command(name="cancel")
|
@announce.command(name="cancel")
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def announce_cancel(self, ctx):
|
async def announce_cancel(self, ctx):
|
||||||
"""
|
"""Cancel a running announce."""
|
||||||
Cancels a running announce.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
self.__current_announcer.cancel()
|
self.__current_announcer.cancel()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
await ctx.send("The current announcement has been cancelled.")
|
await ctx.send(_("The current announcement has been cancelled."))
|
||||||
|
|
||||||
@announce.command(name="channel")
|
@announce.command(name="channel")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def announce_channel(self, ctx, *, channel: discord.TextChannel = None):
|
async def announce_channel(self, ctx, *, channel: discord.TextChannel = None):
|
||||||
"""
|
"""Change the channel to which the bot makes announcements."""
|
||||||
Changes the channel on which the bot makes announcements.
|
|
||||||
"""
|
|
||||||
if channel is None:
|
if channel is None:
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
await self.conf.guild(ctx.guild).announce_channel.set(channel.id)
|
await self.conf.guild(ctx.guild).announce_channel.set(channel.id)
|
||||||
|
|
||||||
await ctx.send("The announcement channel has been set to {}".format(channel.mention))
|
await ctx.send(
|
||||||
|
_("The announcement channel has been set to {channel.mention}").format(channel=channel)
|
||||||
|
)
|
||||||
|
|
||||||
@announce.command(name="ignore")
|
@announce.command(name="ignore")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def announce_ignore(self, ctx):
|
async def announce_ignore(self, ctx):
|
||||||
"""
|
"""Toggle announcements being enabled this server."""
|
||||||
Toggles whether the announcements will ignore the current server.
|
|
||||||
"""
|
|
||||||
ignored = await self.conf.guild(ctx.guild).announce_ignore()
|
ignored = await self.conf.guild(ctx.guild).announce_ignore()
|
||||||
await self.conf.guild(ctx.guild).announce_ignore.set(not ignored)
|
await self.conf.guild(ctx.guild).announce_ignore.set(not ignored)
|
||||||
|
|
||||||
verb = "will" if ignored else "will not"
|
if ignored: # Keeping original logic....
|
||||||
|
await ctx.send(
|
||||||
await ctx.send(f"The server {ctx.guild.name} {verb} receive announcements.")
|
_("The server {guild.name} will receive announcements.").format(guild=ctx.guild)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await ctx.send(
|
||||||
|
_("The server {guild.name} will not receive announcements.").format(
|
||||||
|
guild=ctx.guild
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]:
|
async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]:
|
||||||
"""
|
"""
|
||||||
@@ -294,8 +308,9 @@ class Admin:
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.group(invoke_without_command=True)
|
@commands.group(invoke_without_command=True)
|
||||||
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
|
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||||
"""
|
"""Add a role to yourself.
|
||||||
Add a role to yourself that server admins have configured as user settable.
|
|
||||||
|
Server admins must have configured the role as user settable.
|
||||||
|
|
||||||
NOTE: The role is case sensitive!
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
@@ -304,8 +319,7 @@ class Admin:
|
|||||||
|
|
||||||
@selfrole.command(name="remove")
|
@selfrole.command(name="remove")
|
||||||
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
|
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||||
"""
|
"""Remove a selfrole from yourself.
|
||||||
Removes a selfrole from yourself.
|
|
||||||
|
|
||||||
NOTE: The role is case sensitive!
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
@@ -315,8 +329,7 @@ class Admin:
|
|||||||
@selfrole.command(name="add")
|
@selfrole.command(name="add")
|
||||||
@checks.admin_or_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role):
|
async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role):
|
||||||
"""
|
"""Add a role to the list of available selfroles.
|
||||||
Add a role to the list of available selfroles.
|
|
||||||
|
|
||||||
NOTE: The role is case sensitive!
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
@@ -324,20 +337,19 @@ class Admin:
|
|||||||
if role.id not in curr_selfroles:
|
if role.id not in curr_selfroles:
|
||||||
curr_selfroles.append(role.id)
|
curr_selfroles.append(role.id)
|
||||||
|
|
||||||
await ctx.send("The selfroles list has been successfully modified.")
|
await ctx.send(_("The selfroles list has been successfully modified."))
|
||||||
|
|
||||||
@selfrole.command(name="delete")
|
@selfrole.command(name="delete")
|
||||||
@checks.admin_or_permissions(manage_roles=True)
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole):
|
async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole):
|
||||||
"""
|
"""Remove a role from the list of available selfroles.
|
||||||
Removes a role from the list of available selfroles.
|
|
||||||
|
|
||||||
NOTE: The role is case sensitive!
|
NOTE: The role is case sensitive!
|
||||||
"""
|
"""
|
||||||
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
||||||
curr_selfroles.remove(role.id)
|
curr_selfroles.remove(role.id)
|
||||||
|
|
||||||
await ctx.send("The selfroles list has been successfully modified.")
|
await ctx.send(_("The selfroles list has been successfully modified."))
|
||||||
|
|
||||||
@selfrole.command(name="list")
|
@selfrole.command(name="list")
|
||||||
async def selfrole_list(self, ctx: commands.Context):
|
async def selfrole_list(self, ctx: commands.Context):
|
||||||
@@ -347,7 +359,7 @@ class Admin:
|
|||||||
selfroles = await self._valid_selfroles(ctx.guild)
|
selfroles = await self._valid_selfroles(ctx.guild)
|
||||||
fmt_selfroles = "\n".join(["+ " + r.name for r in selfroles])
|
fmt_selfroles = "\n".join(["+ " + r.name for r in selfroles])
|
||||||
|
|
||||||
msg = "Available Selfroles:\n{}".format(fmt_selfroles)
|
msg = _("Available Selfroles: {selfroles}").format(selfroles=fmt_selfroles)
|
||||||
await ctx.send(box(msg, "diff"))
|
await ctx.send(box(msg, "diff"))
|
||||||
|
|
||||||
async def _serverlock_check(self, guild: discord.Guild) -> bool:
|
async def _serverlock_check(self, guild: discord.Guild) -> bool:
|
||||||
@@ -364,15 +376,14 @@ class Admin:
|
|||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def serverlock(self, ctx: commands.Context):
|
async def serverlock(self, ctx: commands.Context):
|
||||||
"""
|
"""Lock a bot to its current servers only."""
|
||||||
Locks a bot to its current servers only.
|
|
||||||
"""
|
|
||||||
serverlocked = await self.conf.serverlocked()
|
serverlocked = await self.conf.serverlocked()
|
||||||
await self.conf.serverlocked.set(not serverlocked)
|
await self.conf.serverlocked.set(not serverlocked)
|
||||||
|
|
||||||
verb = "is now" if not serverlocked else "is no longer"
|
if serverlocked:
|
||||||
|
await ctx.send(_("The bot is no longer serverlocked."))
|
||||||
await ctx.send("The bot {} serverlocked.".format(verb))
|
else:
|
||||||
|
await ctx.send(_("The bot is now serverlocked."))
|
||||||
|
|
||||||
# region Event Handlers
|
# region Event Handlers
|
||||||
async def on_guild_join(self, guild: discord.Guild):
|
async def on_guild_join(self, guild: discord.Guild):
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import asyncio
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
|
from redbot.core.i18n import Translator
|
||||||
|
|
||||||
|
_ = Translator("Announcer", __file__)
|
||||||
|
|
||||||
|
|
||||||
class Announcer:
|
class Announcer:
|
||||||
@@ -63,7 +66,9 @@ class Announcer:
|
|||||||
try:
|
try:
|
||||||
await channel.send(self.message)
|
await channel.send(self.message)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await bot_owner.send("I could not announce to server: {}".format(g.id))
|
await bot_owner.send(
|
||||||
|
_("I could not announce to server: {server.id}").format(server=g)
|
||||||
|
)
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import discord
|
import discord
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
|
from redbot.core.i18n import Translator
|
||||||
|
|
||||||
|
_ = Translator("AdminConverters", __file__)
|
||||||
|
|
||||||
|
|
||||||
class MemberDefaultAuthor(commands.Converter):
|
class MemberDefaultAuthor(commands.Converter):
|
||||||
@@ -19,7 +22,7 @@ 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.instance
|
||||||
if admin is None:
|
if admin is None:
|
||||||
raise commands.BadArgument("Admin is not loaded.")
|
raise commands.BadArgument(_("The Admin cog is not loaded."))
|
||||||
|
|
||||||
conf = admin.conf
|
conf = admin.conf
|
||||||
selfroles = await conf.guild(ctx.guild).selfroles()
|
selfroles = await conf.guild(ctx.guild).selfroles()
|
||||||
@@ -28,5 +31,5 @@ class SelfRole(commands.Converter):
|
|||||||
role = await role_converter.convert(ctx, arg)
|
role = await role_converter.convert(ctx, arg)
|
||||||
|
|
||||||
if role.id not in selfroles:
|
if role.id not in selfroles:
|
||||||
raise commands.BadArgument("The provided role is not a valid selfrole.")
|
raise commands.BadArgument(_("The provided role is not a valid selfrole."))
|
||||||
return role
|
return role
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from copy import copy
|
from copy import copy
|
||||||
from re import search
|
from re import search
|
||||||
from typing import Generator, Tuple, Iterable
|
from typing import Generator, Tuple, Iterable, Optional
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import Config, commands, checks
|
from redbot.core import Config, commands, checks
|
||||||
@@ -14,16 +14,15 @@ _ = Translator("Alias", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Alias:
|
class Alias(commands.Cog):
|
||||||
"""
|
"""Create aliases for commands.
|
||||||
Alias
|
|
||||||
|
Aliases are alternative names shortcuts for commands. They
|
||||||
Aliases are per server shortcuts for commands. They
|
can act as both a lambda (storing arguments for repeated use)
|
||||||
can act as both a lambda (storing arguments for repeated use)
|
or as simply a shortcut to saying "x y z".
|
||||||
or as simply a shortcut to saying "x y z".
|
|
||||||
|
|
||||||
When run, aliases will accept any additional arguments
|
When run, aliases will accept any additional arguments
|
||||||
and append them to the stored alias
|
and append them to the stored alias.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
default_global_settings = {"entries": []}
|
default_global_settings = {"entries": []}
|
||||||
@@ -31,6 +30,7 @@ class Alias:
|
|||||||
default_guild_settings = {"enabled": False, "entries": []} # Going to be a list of dicts
|
default_guild_settings = {"enabled": False, "entries": []} # Going to be a list of dicts
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self._aliases = Config.get_conf(self, 8927348724)
|
self._aliases = Config.get_conf(self, 8927348724)
|
||||||
|
|
||||||
@@ -53,10 +53,13 @@ class Alias:
|
|||||||
return (AliasEntry.from_json(d, bot=self.bot) for d in (await self._aliases.entries()))
|
return (AliasEntry.from_json(d, bot=self.bot) for d in (await self._aliases.entries()))
|
||||||
|
|
||||||
async def is_alias(
|
async def is_alias(
|
||||||
self, guild: discord.Guild, alias_name: str, server_aliases: Iterable[AliasEntry] = ()
|
self,
|
||||||
) -> (bool, AliasEntry):
|
guild: Optional[discord.Guild],
|
||||||
|
alias_name: str,
|
||||||
|
server_aliases: Iterable[AliasEntry] = (),
|
||||||
|
) -> Tuple[bool, Optional[AliasEntry]]:
|
||||||
|
|
||||||
if not server_aliases:
|
if not server_aliases and guild is not None:
|
||||||
server_aliases = await self.unloaded_aliases(guild)
|
server_aliases = await self.unloaded_aliases(guild)
|
||||||
|
|
||||||
global_aliases = await self.unloaded_global_aliases()
|
global_aliases = await self.unloaded_global_aliases()
|
||||||
@@ -173,32 +176,28 @@ class Alias:
|
|||||||
@commands.group()
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def alias(self, ctx: commands.Context):
|
async def alias(self, ctx: commands.Context):
|
||||||
"""Manage per-server aliases for commands"""
|
"""Manage command aliases."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@alias.group(name="global")
|
@alias.group(name="global")
|
||||||
async def global_(self, ctx: commands.Context):
|
async def global_(self, ctx: commands.Context):
|
||||||
"""
|
"""Manage global aliases."""
|
||||||
Manage global aliases.
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@checks.mod_or_permissions(manage_guild=True)
|
@checks.mod_or_permissions(manage_guild=True)
|
||||||
@alias.command(name="add")
|
@alias.command(name="add")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _add_alias(self, ctx: commands.Context, alias_name: str, *, command):
|
async def _add_alias(self, ctx: commands.Context, alias_name: str, *, command):
|
||||||
"""
|
"""Add an alias for a command."""
|
||||||
Add an alias for a command.
|
|
||||||
"""
|
|
||||||
# region Alias Add Validity Checking
|
# region Alias Add Validity Checking
|
||||||
is_command = self.is_command(alias_name)
|
is_command = self.is_command(alias_name)
|
||||||
if is_command:
|
if is_command:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"You attempted to create a new alias"
|
"You attempted to create a new alias"
|
||||||
" with the name {} but that"
|
" with the name {name} but that"
|
||||||
" name is already a command on this bot."
|
" name is already a command on this bot."
|
||||||
).format(alias_name)
|
).format(name=alias_name)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -207,9 +206,9 @@ class Alias:
|
|||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"You attempted to create a new alias"
|
"You attempted to create a new alias"
|
||||||
" with the name {} but that"
|
" with the name {name} but that"
|
||||||
" alias already exists on this server."
|
" alias already exists on this server."
|
||||||
).format(alias_name)
|
).format(name=alias_name)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -218,10 +217,10 @@ class Alias:
|
|||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"You attempted to create a new alias"
|
"You attempted to create a new alias"
|
||||||
" with the name {} but that"
|
" with the name {name} but that"
|
||||||
" name is an invalid alias name. Alias"
|
" name is an invalid alias name. Alias"
|
||||||
" names may not contain spaces."
|
" names may not contain spaces."
|
||||||
).format(alias_name)
|
).format(name=alias_name)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
# endregion
|
# endregion
|
||||||
@@ -231,23 +230,23 @@ class Alias:
|
|||||||
|
|
||||||
await self.add_alias(ctx, alias_name, command)
|
await self.add_alias(ctx, alias_name, command)
|
||||||
|
|
||||||
await ctx.send(_("A new alias with the trigger `{}` has been created.").format(alias_name))
|
await ctx.send(
|
||||||
|
_("A new alias with the trigger `{name}` has been created.").format(name=alias_name)
|
||||||
|
)
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@global_.command(name="add")
|
@global_.command(name="add")
|
||||||
async def _add_global_alias(self, ctx: commands.Context, alias_name: str, *, command):
|
async def _add_global_alias(self, ctx: commands.Context, alias_name: str, *, command):
|
||||||
"""
|
"""Add a global alias for a command."""
|
||||||
Add a global alias for a command.
|
|
||||||
"""
|
|
||||||
# region Alias Add Validity Checking
|
# region Alias Add Validity Checking
|
||||||
is_command = self.is_command(alias_name)
|
is_command = self.is_command(alias_name)
|
||||||
if is_command:
|
if is_command:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"You attempted to create a new global alias"
|
"You attempted to create a new global alias"
|
||||||
" with the name {} but that"
|
" with the name {name} but that"
|
||||||
" name is already a command on this bot."
|
" name is already a command on this bot."
|
||||||
).format(alias_name)
|
).format(name=alias_name)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -256,9 +255,9 @@ class Alias:
|
|||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"You attempted to create a new global alias"
|
"You attempted to create a new global alias"
|
||||||
" with the name {} but that"
|
" with the name {name} but that"
|
||||||
" alias already exists on this server."
|
" alias already exists on this server."
|
||||||
).format(alias_name)
|
).format(name=alias_name)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -267,10 +266,10 @@ class Alias:
|
|||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"You attempted to create a new global alias"
|
"You attempted to create a new global alias"
|
||||||
" with the name {} but that"
|
" with the name {name} but that"
|
||||||
" name is an invalid alias name. Alias"
|
" name is an invalid alias name. Alias"
|
||||||
" names may not contain spaces."
|
" names may not contain spaces."
|
||||||
).format(alias_name)
|
).format(name=alias_name)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
# endregion
|
# endregion
|
||||||
@@ -278,63 +277,65 @@ class Alias:
|
|||||||
await self.add_alias(ctx, alias_name, command, global_=True)
|
await self.add_alias(ctx, alias_name, command, global_=True)
|
||||||
|
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("A new global alias with the trigger `{}` has been created.").format(alias_name)
|
_("A new global alias with the trigger `{name}` has been created.").format(
|
||||||
|
name=alias_name
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@alias.command(name="help")
|
@alias.command(name="help")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _help_alias(self, ctx: commands.Context, alias_name: str):
|
async def _help_alias(self, ctx: commands.Context, alias_name: str):
|
||||||
"""Tries to execute help for the base command of the alias"""
|
"""Try to execute help for the base command of the alias."""
|
||||||
is_alias, alias = await self.is_alias(ctx.guild, alias_name=alias_name)
|
is_alias, alias = await self.is_alias(ctx.guild, alias_name=alias_name)
|
||||||
if is_alias:
|
if is_alias:
|
||||||
base_cmd = alias.command[0]
|
base_cmd = alias.command[0]
|
||||||
|
|
||||||
new_msg = copy(ctx.message)
|
new_msg = copy(ctx.message)
|
||||||
new_msg.content = "{}help {}".format(ctx.prefix, base_cmd)
|
new_msg.content = _("{prefix}help {command}").format(
|
||||||
|
prefix=ctx.prefix, command=base_cmd
|
||||||
|
)
|
||||||
await self.bot.process_commands(new_msg)
|
await self.bot.process_commands(new_msg)
|
||||||
else:
|
else:
|
||||||
ctx.send(_("No such alias exists."))
|
await ctx.send(_("No such alias exists."))
|
||||||
|
|
||||||
@alias.command(name="show")
|
@alias.command(name="show")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _show_alias(self, ctx: commands.Context, alias_name: str):
|
async def _show_alias(self, ctx: commands.Context, alias_name: str):
|
||||||
"""Shows what command the alias executes."""
|
"""Show what command the alias executes."""
|
||||||
is_alias, alias = await self.is_alias(ctx.guild, alias_name)
|
is_alias, alias = await self.is_alias(ctx.guild, alias_name)
|
||||||
|
|
||||||
if is_alias:
|
if is_alias:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The `{}` alias will execute the command `{}`").format(alias_name, alias.command)
|
_("The `{alias_name}` alias will execute the command `{command}`").format(
|
||||||
|
alias_name=alias_name, command=alias.command
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("There is no alias with the name `{}`").format(alias_name))
|
await ctx.send(_("There is no alias with the name `{name}`").format(name=alias_name))
|
||||||
|
|
||||||
@checks.mod_or_permissions(manage_guild=True)
|
@checks.mod_or_permissions(manage_guild=True)
|
||||||
@alias.command(name="del")
|
@alias.command(name="del")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _del_alias(self, ctx: commands.Context, alias_name: str):
|
async def _del_alias(self, ctx: commands.Context, alias_name: str):
|
||||||
"""
|
"""Delete an existing alias on this server."""
|
||||||
Deletes an existing alias on this server.
|
|
||||||
"""
|
|
||||||
aliases = await self.unloaded_aliases(ctx.guild)
|
aliases = await self.unloaded_aliases(ctx.guild)
|
||||||
try:
|
try:
|
||||||
next(aliases)
|
next(aliases)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
await ctx.send(_("There are no aliases on this guild."))
|
await ctx.send(_("There are no aliases on this server."))
|
||||||
return
|
return
|
||||||
|
|
||||||
if await self.delete_alias(ctx, alias_name):
|
if await self.delete_alias(ctx, alias_name):
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Alias with the name `{}` was successfully deleted.").format(alias_name)
|
_("Alias with the name `{name}` was successfully deleted.").format(name=alias_name)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
await ctx.send(_("Alias with name `{name}` was not found.").format(name=alias_name))
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@global_.command(name="del")
|
@global_.command(name="del")
|
||||||
async def _del_global_alias(self, ctx: commands.Context, alias_name: str):
|
async def _del_global_alias(self, ctx: commands.Context, alias_name: str):
|
||||||
"""
|
"""Delete an existing global alias."""
|
||||||
Deletes an existing global alias.
|
|
||||||
"""
|
|
||||||
aliases = await self.unloaded_global_aliases()
|
aliases = await self.unloaded_global_aliases()
|
||||||
try:
|
try:
|
||||||
next(aliases)
|
next(aliases)
|
||||||
@@ -344,17 +345,15 @@ class Alias:
|
|||||||
|
|
||||||
if await self.delete_alias(ctx, alias_name, global_=True):
|
if await self.delete_alias(ctx, alias_name, global_=True):
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Alias with the name `{}` was successfully deleted.").format(alias_name)
|
_("Alias with the name `{name}` was successfully deleted.").format(name=alias_name)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
await ctx.send(_("Alias with name `{name}` was not found.").format(name=alias_name))
|
||||||
|
|
||||||
@alias.command(name="list")
|
@alias.command(name="list")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def _list_alias(self, ctx: commands.Context):
|
async def _list_alias(self, ctx: commands.Context):
|
||||||
"""
|
"""List the available aliases on this server."""
|
||||||
Lists the available aliases on this server.
|
|
||||||
"""
|
|
||||||
names = [_("Aliases:")] + sorted(
|
names = [_("Aliases:")] + sorted(
|
||||||
["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))]
|
["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))]
|
||||||
)
|
)
|
||||||
@@ -365,9 +364,7 @@ class Alias:
|
|||||||
|
|
||||||
@global_.command(name="list")
|
@global_.command(name="list")
|
||||||
async def _list_global_alias(self, ctx: commands.Context):
|
async def _list_global_alias(self, ctx: commands.Context):
|
||||||
"""
|
"""List the available global aliases on this bot."""
|
||||||
Lists the available global aliases on this bot.
|
|
||||||
"""
|
|
||||||
names = [_("Aliases:")] + sorted(
|
names = [_("Aliases:")] + sorted(
|
||||||
["+ " + a.name for a in await self.unloaded_global_aliases()]
|
["+ " + a.name for a in await self.unloaded_global_aliases()]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -52,6 +52,6 @@ async def setup(bot: commands.Bot):
|
|||||||
await maybe_download_lavalink(bot.loop, cog)
|
await maybe_download_lavalink(bot.loop, cog)
|
||||||
await start_lavalink_server(bot.loop)
|
await start_lavalink_server(bot.loop)
|
||||||
|
|
||||||
|
await cog.initialize()
|
||||||
|
|
||||||
bot.add_cog(cog)
|
bot.add_cog(cog)
|
||||||
bot.loop.create_task(cog.disconnect_timer())
|
|
||||||
bot.loop.create_task(cog.init_config())
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -54,10 +54,11 @@ def check_global_setting_admin():
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Bank:
|
class Bank(commands.Cog):
|
||||||
"""Bank"""
|
"""Bank"""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
# SECTION commands
|
# SECTION commands
|
||||||
@@ -66,7 +67,7 @@ class Bank:
|
|||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
@commands.group(autohelp=True)
|
@commands.group(autohelp=True)
|
||||||
async def bankset(self, ctx: commands.Context):
|
async def bankset(self, ctx: commands.Context):
|
||||||
"""Base command for bank settings"""
|
"""Base command for bank settings."""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
if await bank.is_global():
|
if await bank.is_global():
|
||||||
bank_name = await bank._conf.bank_name()
|
bank_name = await bank._conf.bank_name()
|
||||||
@@ -80,42 +81,47 @@ class Bank:
|
|||||||
default_balance = await bank._conf.guild(ctx.guild).default_balance()
|
default_balance = await bank._conf.guild(ctx.guild).default_balance()
|
||||||
|
|
||||||
settings = _(
|
settings = _(
|
||||||
"Bank settings:\n\nBank name: {}\nCurrency: {}\nDefault balance: {}"
|
"Bank settings:\n\nBank name: {bank_name}\nCurrency: {currency_name}\n"
|
||||||
).format(bank_name, currency_name, default_balance)
|
"Default balance: {default_balance}"
|
||||||
|
).format(
|
||||||
|
bank_name=bank_name, currency_name=currency_name, default_balance=default_balance
|
||||||
|
)
|
||||||
await ctx.send(box(settings))
|
await ctx.send(box(settings))
|
||||||
|
|
||||||
@bankset.command(name="toggleglobal")
|
@bankset.command(name="toggleglobal")
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def bankset_toggleglobal(self, ctx: commands.Context, confirm: bool = False):
|
async def bankset_toggleglobal(self, ctx: commands.Context, confirm: bool = False):
|
||||||
"""Toggles whether the bank is global or not
|
"""Toggle whether the bank is global or not.
|
||||||
If the bank is global, it will become per-server
|
|
||||||
If the bank is per-server, it will become global"""
|
If the bank is global, it will become per-server.
|
||||||
|
If the bank is per-server, it will become global.
|
||||||
|
"""
|
||||||
cur_setting = await bank.is_global()
|
cur_setting = await bank.is_global()
|
||||||
|
|
||||||
word = _("per-server") if cur_setting else _("global")
|
word = _("per-server") if cur_setting else _("global")
|
||||||
if confirm is False:
|
if confirm is False:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"This will toggle the bank to be {}, deleting all accounts "
|
"This will toggle the bank to be {banktype}, deleting all accounts "
|
||||||
"in the process! If you're sure, type `{}`"
|
"in the process! If you're sure, type `{command}`"
|
||||||
).format(word, "{}bankset toggleglobal yes".format(ctx.prefix))
|
).format(banktype=word, command="{}bankset toggleglobal yes".format(ctx.prefix))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await bank.set_global(not cur_setting)
|
await bank.set_global(not cur_setting)
|
||||||
await ctx.send(_("The bank is now {}.").format(word))
|
await ctx.send(_("The bank is now {banktype}.").format(banktype=word))
|
||||||
|
|
||||||
@bankset.command(name="bankname")
|
@bankset.command(name="bankname")
|
||||||
@check_global_setting_guildowner()
|
@check_global_setting_guildowner()
|
||||||
async def bankset_bankname(self, ctx: commands.Context, *, name: str):
|
async def bankset_bankname(self, ctx: commands.Context, *, name: str):
|
||||||
"""Set the bank's name"""
|
"""Set the bank's name."""
|
||||||
await bank.set_bank_name(name, ctx.guild)
|
await bank.set_bank_name(name, ctx.guild)
|
||||||
await ctx.send(_("Bank's name has been set to {}").format(name))
|
await ctx.send(_("Bank name has been set to: {name}").format(name=name))
|
||||||
|
|
||||||
@bankset.command(name="creditsname")
|
@bankset.command(name="creditsname")
|
||||||
@check_global_setting_guildowner()
|
@check_global_setting_guildowner()
|
||||||
async def bankset_creditsname(self, ctx: commands.Context, *, name: str):
|
async def bankset_creditsname(self, ctx: commands.Context, *, name: str):
|
||||||
"""Set the name for the bank's currency"""
|
"""Set the name for the bank's currency."""
|
||||||
await bank.set_currency_name(name, ctx.guild)
|
await bank.set_currency_name(name, ctx.guild)
|
||||||
await ctx.send(_("Currency name has been set to {}").format(name))
|
await ctx.send(_("Currency name has been set to: {name}").format(name=name))
|
||||||
|
|
||||||
# ENDSECTION
|
# ENDSECTION
|
||||||
|
|||||||
@@ -9,15 +9,17 @@ from redbot.core.bot import Red
|
|||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.mod import slow_deletion, mass_purge
|
from redbot.core.utils.mod import slow_deletion, mass_purge
|
||||||
from redbot.cogs.mod.log import log
|
from redbot.cogs.mod.log import log
|
||||||
|
from redbot.core.utils.predicates import MessagePredicate
|
||||||
|
|
||||||
_ = Translator("Cleanup", __file__)
|
_ = Translator("Cleanup", __file__)
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Cleanup:
|
class Cleanup(commands.Cog):
|
||||||
"""Commands for cleaning messages"""
|
"""Commands for cleaning up messages."""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -30,19 +32,16 @@ class Cleanup:
|
|||||||
Tries its best to cleanup after itself if the response is positive.
|
Tries its best to cleanup after itself if the response is positive.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def author_check(message):
|
|
||||||
return message.author == ctx.author
|
|
||||||
|
|
||||||
prompt = await ctx.send(
|
prompt = await ctx.send(
|
||||||
_("Are you sure you want to delete {} messages? (y/n)").format(number)
|
_("Are you sure you want to delete {number} messages? (y/n)").format(number=number)
|
||||||
)
|
)
|
||||||
response = await ctx.bot.wait_for("message", check=author_check)
|
response = await ctx.bot.wait_for("message", check=MessagePredicate.same_context(ctx))
|
||||||
|
|
||||||
if response.content.lower().startswith("y"):
|
if response.content.lower().startswith("y"):
|
||||||
await prompt.delete()
|
await prompt.delete()
|
||||||
try:
|
try:
|
||||||
await response.delete()
|
await response.delete()
|
||||||
except:
|
except discord.HTTPException:
|
||||||
pass
|
pass
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@@ -105,25 +104,24 @@ class Cleanup:
|
|||||||
@commands.group()
|
@commands.group()
|
||||||
@checks.mod_or_permissions(manage_messages=True)
|
@checks.mod_or_permissions(manage_messages=True)
|
||||||
async def cleanup(self, ctx: commands.Context):
|
async def cleanup(self, ctx: commands.Context):
|
||||||
"""Deletes messages."""
|
"""Delete messages."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
async def text(
|
async def text(
|
||||||
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
|
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
|
||||||
):
|
):
|
||||||
"""Deletes last X messages matching the specified text.
|
"""Delete the last X messages matching the specified text.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
cleanup text \"test\" 5
|
`[p]cleanup text "test" 5`
|
||||||
|
|
||||||
Remember to use double quotes."""
|
Remember to use double quotes.
|
||||||
|
"""
|
||||||
|
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
|
||||||
await ctx.send("I need the Manage Messages permission to do this.")
|
|
||||||
return
|
|
||||||
|
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
|
|
||||||
@@ -157,18 +155,17 @@ class Cleanup:
|
|||||||
|
|
||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
async def user(
|
async def user(
|
||||||
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
|
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
|
||||||
):
|
):
|
||||||
"""Deletes last X messages from specified user.
|
"""Delete the last X messages from a specified user.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
cleanup user @\u200bTwentysix 2
|
`[p]cleanup user @\u200bTwentysix 2`
|
||||||
cleanup user Red 6"""
|
`[p]cleanup user Red 6`
|
||||||
|
"""
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
|
||||||
await ctx.send("I need the Manage Messages permission to do this.")
|
|
||||||
return
|
|
||||||
|
|
||||||
member = None
|
member = None
|
||||||
try:
|
try:
|
||||||
@@ -214,20 +211,16 @@ class Cleanup:
|
|||||||
|
|
||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
|
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
|
||||||
"""Deletes all messages after specified message.
|
"""Delete all messages after a specified message.
|
||||||
|
|
||||||
To get a message id, enable developer mode in Discord's
|
To get a message id, enable developer mode in Discord's
|
||||||
settings, 'appearance' tab. Then right click a message
|
settings, 'appearance' tab. Then right click a message
|
||||||
and copy its id.
|
and copy its id.
|
||||||
|
|
||||||
This command only works on bots running as bot accounts.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
|
||||||
await ctx.send("I need the Manage Messages permission to do this.")
|
|
||||||
return
|
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -248,16 +241,47 @@ class Cleanup:
|
|||||||
|
|
||||||
@cleanup.command()
|
@cleanup.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
"""Deletes last X messages.
|
async def before(
|
||||||
|
self, ctx: commands.Context, message_id: int, number: int, delete_pinned: bool = False
|
||||||
|
):
|
||||||
|
"""Deletes X messages before specified message.
|
||||||
|
|
||||||
Example:
|
To get a message id, enable developer mode in Discord's
|
||||||
cleanup messages 26"""
|
settings, 'appearance' tab. Then right click a message
|
||||||
|
and copy its id.
|
||||||
|
"""
|
||||||
|
|
||||||
|
channel = ctx.channel
|
||||||
|
author = ctx.author
|
||||||
|
|
||||||
|
try:
|
||||||
|
before = await channel.get_message(message_id)
|
||||||
|
except discord.NotFound:
|
||||||
|
return await ctx.send(_("Message not found."))
|
||||||
|
|
||||||
|
to_delete = await self.get_messages_for_deletion(
|
||||||
|
channel=channel, number=number, before=before, delete_pinned=delete_pinned
|
||||||
|
)
|
||||||
|
|
||||||
|
reason = "{}({}) deleted {} messages in channel {}.".format(
|
||||||
|
author.name, author.id, len(to_delete), channel.name
|
||||||
|
)
|
||||||
|
log.info(reason)
|
||||||
|
|
||||||
|
await mass_purge(to_delete, channel)
|
||||||
|
|
||||||
|
@cleanup.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
|
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||||
|
"""Delete the last X messages.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
`[p]cleanup messages 26`
|
||||||
|
"""
|
||||||
|
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
|
||||||
await ctx.send("I need the Manage Messages permission to do this.")
|
|
||||||
return
|
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
|
|
||||||
if number > 100:
|
if number > 100:
|
||||||
@@ -279,13 +303,11 @@ class Cleanup:
|
|||||||
|
|
||||||
@cleanup.command(name="bot")
|
@cleanup.command(name="bot")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||||
"""Cleans up command messages and messages from the bot."""
|
"""Clean up command messages and messages from the bot."""
|
||||||
|
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
|
||||||
await ctx.send("I need the Manage Messages permission to do this.")
|
|
||||||
return
|
|
||||||
author = ctx.message.author
|
author = ctx.message.author
|
||||||
|
|
||||||
if number > 100:
|
if number > 100:
|
||||||
@@ -338,7 +360,7 @@ class Cleanup:
|
|||||||
match_pattern: str = None,
|
match_pattern: str = None,
|
||||||
delete_pinned: bool = False,
|
delete_pinned: bool = False,
|
||||||
):
|
):
|
||||||
"""Cleans up messages owned by the bot.
|
"""Clean up messages owned by the bot.
|
||||||
|
|
||||||
By default, all messages are cleaned. If a third argument is specified,
|
By default, all messages are cleaned. If a third argument is specified,
|
||||||
it is used for pattern matching: If it begins with r( and ends with ),
|
it is used for pattern matching: If it begins with r( and ends with ),
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import random
|
import random
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from inspect import Parameter
|
from inspect import Parameter
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Mapping
|
from typing import Mapping, Tuple, Dict
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from redbot.core import Config, checks, commands
|
from redbot.core import Config, checks, commands
|
||||||
from redbot.core.utils.chat_formatting import box, pagify
|
from redbot.core.utils.chat_formatting import box, pagify
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
|
from redbot.core.utils.predicates import MessagePredicate
|
||||||
|
|
||||||
_ = Translator("CustomCommands", __file__)
|
_ = Translator("CustomCommands", __file__)
|
||||||
|
|
||||||
@@ -19,10 +19,6 @@ class CCError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NotFound(CCError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AlreadyExists(CCError):
|
class AlreadyExists(CCError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -31,6 +27,14 @@ class ArgParseError(CCError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(CCError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OnCooldown(CCError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommandObj:
|
class CommandObj:
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
config = kwargs.get("config")
|
config = kwargs.get("config")
|
||||||
@@ -47,21 +51,18 @@ class CommandObj:
|
|||||||
|
|
||||||
async def get_responses(self, ctx):
|
async def get_responses(self, ctx):
|
||||||
intro = _(
|
intro = _(
|
||||||
"Welcome to the interactive random {} maker!\n"
|
"Welcome to the interactive random {cc} maker!\n"
|
||||||
"Every message you send will be added as one of the random "
|
"Every message you send will be added as one of the random "
|
||||||
"responses to choose from once this {} is "
|
"responses to choose from once this {cc} is "
|
||||||
"triggered. To exit this interactive menu, type `{}`"
|
"triggered. To exit this interactive menu, type `{quit}`"
|
||||||
).format("customcommand", "customcommand", "exit()")
|
).format(cc="customcommand", quit="exit()")
|
||||||
await ctx.send(intro)
|
await ctx.send(intro)
|
||||||
|
|
||||||
def check(m):
|
|
||||||
return m.channel == ctx.channel and m.author == ctx.message.author
|
|
||||||
|
|
||||||
responses = []
|
responses = []
|
||||||
args = None
|
args = None
|
||||||
while True:
|
while True:
|
||||||
await ctx.send(_("Add a random response:"))
|
await ctx.send(_("Add a random response:"))
|
||||||
msg = await self.bot.wait_for("message", check=check)
|
msg = await self.bot.wait_for("message", check=MessagePredicate.same_context(ctx))
|
||||||
|
|
||||||
if msg.content.lower() == "exit()":
|
if msg.content.lower() == "exit()":
|
||||||
break
|
break
|
||||||
@@ -83,14 +84,14 @@ class CommandObj:
|
|||||||
# in the ccinfo dict
|
# in the ccinfo dict
|
||||||
return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow())
|
return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow())
|
||||||
|
|
||||||
async def get(self, message: discord.Message, command: str) -> str:
|
async def get(self, message: discord.Message, command: str) -> Tuple[str, Dict]:
|
||||||
ccinfo = await self.db(message.guild).commands.get_raw(command, default=None)
|
ccinfo = await self.db(message.guild).commands.get_raw(command, default=None)
|
||||||
if not ccinfo:
|
if not ccinfo:
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
else:
|
else:
|
||||||
return ccinfo["response"]
|
return ccinfo["response"], ccinfo.get("cooldowns", {})
|
||||||
|
|
||||||
async def create(self, ctx: commands.Context, command: str, response):
|
async def create(self, ctx: commands.Context, command: str, *, response):
|
||||||
"""Create a custom command"""
|
"""Create a custom command"""
|
||||||
# Check if this command is already registered as a customcommand
|
# Check if this command is already registered as a customcommand
|
||||||
if await self.db(ctx.guild).commands.get_raw(command, default=None):
|
if await self.db(ctx.guild).commands.get_raw(command, default=None):
|
||||||
@@ -101,45 +102,71 @@ class CommandObj:
|
|||||||
ccinfo = {
|
ccinfo = {
|
||||||
"author": {"id": author.id, "name": author.name},
|
"author": {"id": author.id, "name": author.name},
|
||||||
"command": command,
|
"command": command,
|
||||||
|
"cooldowns": {},
|
||||||
"created_at": self.get_now(),
|
"created_at": self.get_now(),
|
||||||
"editors": [],
|
"editors": [],
|
||||||
"response": response,
|
"response": response,
|
||||||
}
|
}
|
||||||
await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
|
await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
|
||||||
|
|
||||||
async def edit(self, ctx: commands.Context, command: str, response: None):
|
async def edit(
|
||||||
|
self,
|
||||||
|
ctx: commands.Context,
|
||||||
|
command: str,
|
||||||
|
*,
|
||||||
|
response=None,
|
||||||
|
cooldowns: Mapping[str, int] = None,
|
||||||
|
ask_for: bool = True
|
||||||
|
):
|
||||||
"""Edit an already existing custom command"""
|
"""Edit an already existing custom command"""
|
||||||
|
ccinfo = await self.db(ctx.guild).commands.get_raw(command, default=None)
|
||||||
|
|
||||||
# Check if this command is registered
|
# Check if this command is registered
|
||||||
if not await self.db(ctx.guild).commands.get_raw(command, default=None):
|
if not ccinfo:
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
author = ctx.message.author
|
author = ctx.message.author
|
||||||
ccinfo = await self.db(ctx.guild).commands.get_raw(command, default=None)
|
|
||||||
|
|
||||||
def check(m):
|
if ask_for and not response:
|
||||||
return m.channel == ctx.channel and m.author == ctx.message.author
|
await ctx.send(_("Do you want to create a 'randomized' custom command? (y/n)"))
|
||||||
|
|
||||||
if not response:
|
pred = MessagePredicate.yes_or_no(ctx)
|
||||||
await ctx.send(_("Do you want to create a 'randomized' cc? {}").format("y/n"))
|
try:
|
||||||
|
await self.bot.wait_for("message", check=pred, timeout=30)
|
||||||
msg = await self.bot.wait_for("message", check=check)
|
except TimeoutError:
|
||||||
if msg.content.lower() == "y":
|
await ctx.send(_("Response timed out, please try again later."))
|
||||||
|
return
|
||||||
|
if pred.result is True:
|
||||||
response = await self.get_responses(ctx=ctx)
|
response = await self.get_responses(ctx=ctx)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("What response do you want?"))
|
await ctx.send(_("What response do you want?"))
|
||||||
response = (await self.bot.wait_for("message", check=check)).content
|
try:
|
||||||
|
resp = await self.bot.wait_for(
|
||||||
|
"message", check=MessagePredicate.same_context(ctx), timeout=180
|
||||||
|
)
|
||||||
|
except TimeoutError:
|
||||||
|
await ctx.send(_("Response timed out, please try again later."))
|
||||||
|
return
|
||||||
|
response = resp.content
|
||||||
|
|
||||||
# test to raise
|
if response:
|
||||||
ctx.cog.prepare_args(response if isinstance(response, str) else response[0])
|
# test to raise
|
||||||
|
ctx.cog.prepare_args(response if isinstance(response, str) else response[0])
|
||||||
|
ccinfo["response"] = response
|
||||||
|
|
||||||
ccinfo["response"] = response
|
if cooldowns:
|
||||||
ccinfo["edited_at"] = self.get_now()
|
ccinfo.setdefault("cooldowns", {}).update(cooldowns)
|
||||||
|
for key, value in ccinfo["cooldowns"].copy().items():
|
||||||
|
if value <= 0:
|
||||||
|
del ccinfo["cooldowns"][key]
|
||||||
|
|
||||||
if author.id not in ccinfo["editors"]:
|
if author.id not in ccinfo["editors"]:
|
||||||
# Add the person who invoked the `edit` coroutine to the list of
|
# Add the person who invoked the `edit` coroutine to the list of
|
||||||
# editors, if the person is not yet in there
|
# editors, if the person is not yet in there
|
||||||
ccinfo["editors"].append(author.id)
|
ccinfo["editors"].append(author.id)
|
||||||
|
|
||||||
|
ccinfo["edited_at"] = self.get_now()
|
||||||
|
|
||||||
await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
|
await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
|
||||||
|
|
||||||
async def delete(self, ctx: commands.Context, command: str):
|
async def delete(self, ctx: commands.Context, command: str):
|
||||||
@@ -151,89 +178,139 @@ class CommandObj:
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class CustomCommands:
|
class CustomCommands(commands.Cog):
|
||||||
"""Custom commands
|
"""Creates commands used to display text."""
|
||||||
|
|
||||||
Creates commands used to display text"""
|
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.key = 414589031223512
|
self.key = 414589031223512
|
||||||
self.config = Config.get_conf(self, self.key)
|
self.config = Config.get_conf(self, self.key)
|
||||||
self.config.register_guild(commands={})
|
self.config.register_guild(commands={})
|
||||||
self.commandobj = CommandObj(config=self.config, bot=self.bot)
|
self.commandobj = CommandObj(config=self.config, bot=self.bot)
|
||||||
|
self.cooldowns = {}
|
||||||
|
|
||||||
@commands.group(aliases=["cc"])
|
@commands.group(aliases=["cc"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def customcom(self, ctx: commands.Context):
|
async def customcom(self, ctx: commands.Context):
|
||||||
"""Custom commands management"""
|
"""Custom commands management."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@customcom.group(name="add")
|
@customcom.group(name="create", aliases=["add"])
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_add(self, ctx: commands.Context):
|
async def cc_create(self, ctx: commands.Context):
|
||||||
"""
|
"""Create custom commands.
|
||||||
Adds a new custom command
|
|
||||||
|
|
||||||
CCs can be enhanced with arguments:
|
CCs can be enhanced with arguments, see the guide
|
||||||
https://red-discordbot.readthedocs.io/en/v3-develop/cog_customcom.html
|
[here](https://red-discordbot.readthedocs.io/en/v3-develop/cog_customcom.html).
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@cc_add.command(name="random")
|
@cc_create.command(name="random")
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_add_random(self, ctx: commands.Context, command: str):
|
async def cc_create_random(self, ctx: commands.Context, command: str.lower):
|
||||||
"""
|
"""Create a CC where it will randomly choose a response!
|
||||||
Create a CC where it will randomly choose a response!
|
|
||||||
|
|
||||||
Note: This is interactive
|
Note: This command is interactive.
|
||||||
"""
|
"""
|
||||||
responses = []
|
|
||||||
|
|
||||||
responses = await self.commandobj.get_responses(ctx=ctx)
|
responses = await self.commandobj.get_responses(ctx=ctx)
|
||||||
try:
|
try:
|
||||||
await self.commandobj.create(ctx=ctx, command=command, response=responses)
|
await self.commandobj.create(ctx=ctx, command=command, response=responses)
|
||||||
await ctx.send(_("Custom command successfully added."))
|
await ctx.send(_("Custom command successfully added."))
|
||||||
except AlreadyExists:
|
except AlreadyExists:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("This command already exists. Use `{}` to edit it.").format(
|
_("This command already exists. Use `{command}` to edit it.").format(
|
||||||
"{}customcom edit".format(ctx.prefix)
|
command="{}customcom edit".format(ctx.prefix)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# await ctx.send(str(responses))
|
@cc_create.command(name="simple")
|
||||||
|
|
||||||
@cc_add.command(name="simple")
|
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_add_simple(self, ctx, command: str, *, text):
|
async def cc_create_simple(self, ctx, command: str.lower, *, text: str):
|
||||||
"""Adds a simple custom command
|
"""Add a simple custom command.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
[p]customcom add simple yourcommand Text you want
|
- `[p]customcom create simple yourcommand Text you want`
|
||||||
"""
|
"""
|
||||||
command = command.lower()
|
|
||||||
if command in self.bot.all_commands:
|
if command in self.bot.all_commands:
|
||||||
await ctx.send(_("That command is already a standard command."))
|
await ctx.send(_("There already exists a bot command with the same name."))
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
await self.commandobj.create(ctx=ctx, command=command, response=text)
|
await self.commandobj.create(ctx=ctx, command=command, response=text)
|
||||||
await ctx.send(_("Custom command successfully added."))
|
await ctx.send(_("Custom command successfully added."))
|
||||||
except AlreadyExists:
|
except AlreadyExists:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("This command already exists. Use `{}` to edit it.").format(
|
_("This command already exists. Use `{command}` to edit it.").format(
|
||||||
"{}customcom edit".format(ctx.prefix)
|
command="{}customcom edit".format(ctx.prefix)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except ArgParseError as e:
|
except ArgParseError as e:
|
||||||
await ctx.send(e.args[0])
|
await ctx.send(e.args[0])
|
||||||
|
|
||||||
@customcom.command(name="edit")
|
@customcom.command(name="cooldown")
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def cc_edit(self, ctx, command: str, *, text=None):
|
async def cc_cooldown(
|
||||||
"""Edits a custom command
|
self, ctx, command: str.lower, cooldown: int = None, *, per: str.lower = "member"
|
||||||
|
):
|
||||||
|
"""Set, edit, or view the cooldown for a custom command.
|
||||||
|
|
||||||
|
You may set cooldowns per member, channel, or guild. Multiple
|
||||||
|
cooldowns may be set. All cooldowns must be cooled to call the
|
||||||
|
custom command.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
[p]customcom edit yourcommand Text you want
|
- `[p]customcom cooldown yourcommand 30`
|
||||||
|
"""
|
||||||
|
if cooldown is None:
|
||||||
|
try:
|
||||||
|
cooldowns = (await self.commandobj.get(ctx.message, command))[1]
|
||||||
|
except NotFound:
|
||||||
|
return await ctx.send(_("That command doesn't exist."))
|
||||||
|
if cooldowns:
|
||||||
|
cooldown = []
|
||||||
|
for per, rate in cooldowns.items():
|
||||||
|
cooldown.append(
|
||||||
|
_("A {} may call this command every {} seconds").format(per, rate)
|
||||||
|
)
|
||||||
|
return await ctx.send("\n".join(cooldown))
|
||||||
|
else:
|
||||||
|
return await ctx.send(_("This command has no cooldown."))
|
||||||
|
per = {"server": "guild", "user": "member"}.get(per, per)
|
||||||
|
allowed = ("guild", "member", "channel")
|
||||||
|
if per not in allowed:
|
||||||
|
return await ctx.send(_("{} must be one of {}").format("per", ", ".join(allowed)))
|
||||||
|
cooldown = {per: cooldown}
|
||||||
|
try:
|
||||||
|
await self.commandobj.edit(ctx=ctx, command=command, cooldowns=cooldown, ask_for=False)
|
||||||
|
await ctx.send(_("Custom command cooldown successfully edited."))
|
||||||
|
except NotFound:
|
||||||
|
await ctx.send(
|
||||||
|
_("That command doesn't exist. Use `{command}` to add it.").format(
|
||||||
|
command="{}customcom create".format(ctx.prefix)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@customcom.command(name="delete")
|
||||||
|
@checks.mod_or_permissions(administrator=True)
|
||||||
|
async def cc_delete(self, ctx, command: str.lower):
|
||||||
|
"""Delete a custom command
|
||||||
|
.
|
||||||
|
Example:
|
||||||
|
- `[p]customcom delete yourcommand`
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await self.commandobj.delete(ctx=ctx, command=command)
|
||||||
|
await ctx.send(_("Custom command successfully deleted."))
|
||||||
|
except NotFound:
|
||||||
|
await ctx.send(_("That command doesn't exist."))
|
||||||
|
|
||||||
|
@customcom.command(name="edit")
|
||||||
|
@checks.mod_or_permissions(administrator=True)
|
||||||
|
async def cc_edit(self, ctx, command: str.lower, *, text: str = None):
|
||||||
|
"""Edit a custom command.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- `[p]customcom edit yourcommand Text you want`
|
||||||
"""
|
"""
|
||||||
command = command.lower()
|
command = command.lower()
|
||||||
|
|
||||||
@@ -242,29 +319,16 @@ class CustomCommands:
|
|||||||
await ctx.send(_("Custom command successfully edited."))
|
await ctx.send(_("Custom command successfully edited."))
|
||||||
except NotFound:
|
except NotFound:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("That command doesn't exist. Use `{}` to add it.").format(
|
_("That command doesn't exist. Use `{command}` to add it.").format(
|
||||||
"{}customcom add".format(ctx.prefix)
|
command="{}customcom create".format(ctx.prefix)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except ArgParseError as e:
|
except ArgParseError as e:
|
||||||
await ctx.send(e.args[0])
|
await ctx.send(e.args[0])
|
||||||
|
|
||||||
@customcom.command(name="delete")
|
|
||||||
@checks.mod_or_permissions(administrator=True)
|
|
||||||
async def cc_delete(self, ctx, command: str):
|
|
||||||
"""Deletes a custom command
|
|
||||||
Example:
|
|
||||||
[p]customcom delete yourcommand"""
|
|
||||||
command = command.lower()
|
|
||||||
try:
|
|
||||||
await self.commandobj.delete(ctx=ctx, command=command)
|
|
||||||
await ctx.send(_("Custom command successfully deleted."))
|
|
||||||
except NotFound:
|
|
||||||
await ctx.send(_("That command doesn't exist."))
|
|
||||||
|
|
||||||
@customcom.command(name="list")
|
@customcom.command(name="list")
|
||||||
async def cc_list(self, ctx):
|
async def cc_list(self, ctx):
|
||||||
"""Shows custom commands list"""
|
"""List all available custom commands."""
|
||||||
|
|
||||||
response = await CommandObj.get_commands(self.config.guild(ctx.guild))
|
response = await CommandObj.get_commands(self.config.guild(ctx.guild))
|
||||||
|
|
||||||
@@ -272,8 +336,8 @@ class CustomCommands:
|
|||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"There are no custom commands in this server."
|
"There are no custom commands in this server."
|
||||||
" Use `{}` to start adding some."
|
" Use `{command}` to start adding some."
|
||||||
).format("{}customcom add".format(ctx.prefix))
|
).format(command="{}customcom create".format(ctx.prefix))
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -302,8 +366,8 @@ class CustomCommands:
|
|||||||
|
|
||||||
# user_allowed check, will be replaced with self.bot.user_allowed or
|
# user_allowed check, will be replaced with self.bot.user_allowed or
|
||||||
# something similar once it's added
|
# something similar once it's added
|
||||||
|
|
||||||
user_allowed = True
|
user_allowed = True
|
||||||
|
|
||||||
if len(message.content) < 2 or is_private or not user_allowed or message.author.bot:
|
if len(message.content) < 2 or is_private or not user_allowed or message.author.bot:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -313,22 +377,25 @@ class CustomCommands:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw_response = await self.commandobj.get(message=message, command=ctx.invoked_with)
|
raw_response, cooldowns = await self.commandobj.get(
|
||||||
|
message=message, command=ctx.invoked_with
|
||||||
|
)
|
||||||
if isinstance(raw_response, list):
|
if isinstance(raw_response, list):
|
||||||
raw_response = random.choice(raw_response)
|
raw_response = random.choice(raw_response)
|
||||||
elif isinstance(raw_response, str):
|
elif isinstance(raw_response, str):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
except NotFound:
|
if cooldowns:
|
||||||
|
self.test_cooldowns(ctx, ctx.invoked_with, cooldowns)
|
||||||
|
except CCError:
|
||||||
return
|
return
|
||||||
await self.call_cc_command(ctx, raw_response, message)
|
|
||||||
|
|
||||||
async def call_cc_command(self, ctx, raw_response, message) -> None:
|
|
||||||
# wrap the command here so it won't register with the bot
|
# wrap the command here so it won't register with the bot
|
||||||
fake_cc = commands.Command(ctx.invoked_with, self.cc_callback)
|
fake_cc = commands.Command(ctx.invoked_with, self.cc_callback)
|
||||||
fake_cc.params = self.prepare_args(raw_response)
|
fake_cc.params = self.prepare_args(raw_response)
|
||||||
ctx.command = fake_cc
|
ctx.command = fake_cc
|
||||||
|
|
||||||
await self.bot.invoke(ctx)
|
await self.bot.invoke(ctx)
|
||||||
if not ctx.command_failed:
|
if not ctx.command_failed:
|
||||||
await self.cc_command(*ctx.args, **ctx.kwargs, raw_response=raw_response)
|
await self.cc_command(*ctx.args, **ctx.kwargs, raw_response=raw_response)
|
||||||
@@ -382,9 +449,8 @@ class CustomCommands:
|
|||||||
gaps = set(indices).symmetric_difference(range(high + 1))
|
gaps = set(indices).symmetric_difference(range(high + 1))
|
||||||
if gaps:
|
if gaps:
|
||||||
raise ArgParseError(
|
raise ArgParseError(
|
||||||
_("Arguments must be sequential. Missing arguments: {}.").format(
|
_("Arguments must be sequential. Missing arguments: ")
|
||||||
", ".join(str(i + low) for i in gaps)
|
+ ", ".join(str(i + low) for i in gaps)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
fin = [Parameter("_" + str(i), Parameter.POSITIONAL_OR_KEYWORD) for i in range(high + 1)]
|
fin = [Parameter("_" + str(i), Parameter.POSITIONAL_OR_KEYWORD) for i in range(high + 1)]
|
||||||
for arg in args:
|
for arg in args:
|
||||||
@@ -409,8 +475,12 @@ class CustomCommands:
|
|||||||
and anno != fin[index].annotation
|
and anno != fin[index].annotation
|
||||||
):
|
):
|
||||||
raise ArgParseError(
|
raise ArgParseError(
|
||||||
_('Conflicting colon notation for argument {}: "{}" and "{}".').format(
|
_(
|
||||||
index + low, fin[index].annotation.__name__, anno.__name__
|
'Conflicting colon notation for argument {index}: "{name1}" and "{name2}".'
|
||||||
|
).format(
|
||||||
|
index=index + low,
|
||||||
|
name1=fin[index].annotation.__name__,
|
||||||
|
name2=anno.__name__,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if anno is not Parameter.empty:
|
if anno is not Parameter.empty:
|
||||||
@@ -429,6 +499,27 @@ class CustomCommands:
|
|||||||
fin = default + [(p.name, p) for p in fin]
|
fin = default + [(p.name, p) for p in fin]
|
||||||
return OrderedDict(fin)
|
return OrderedDict(fin)
|
||||||
|
|
||||||
|
def test_cooldowns(self, ctx, command, cooldowns):
|
||||||
|
now = datetime.utcnow()
|
||||||
|
new_cooldowns = {}
|
||||||
|
for per, rate in cooldowns.items():
|
||||||
|
if per == "guild":
|
||||||
|
key = (command, ctx.guild)
|
||||||
|
elif per == "channel":
|
||||||
|
key = (command, ctx.guild, ctx.channel)
|
||||||
|
elif per == "member":
|
||||||
|
key = (command, ctx.guild, ctx.author)
|
||||||
|
else:
|
||||||
|
raise ValueError(per)
|
||||||
|
cooldown = self.cooldowns.get(key)
|
||||||
|
if cooldown:
|
||||||
|
cooldown += timedelta(seconds=rate)
|
||||||
|
if cooldown > now:
|
||||||
|
raise OnCooldown()
|
||||||
|
new_cooldowns[key] = now
|
||||||
|
# only update cooldowns if the command isn't on cooldown
|
||||||
|
self.cooldowns.update(new_cooldowns)
|
||||||
|
|
||||||
def transform_arg(self, result, attr, obj) -> str:
|
def transform_arg(self, result, attr, obj) -> str:
|
||||||
attr = attr[1:] # strip initial dot
|
attr = attr[1:] # strip initial dot
|
||||||
if not attr:
|
if not attr:
|
||||||
|
|||||||
@@ -6,29 +6,26 @@ from redbot.core.bot import Red
|
|||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.cogs.dataconverter.core_specs import SpecResolver
|
from redbot.cogs.dataconverter.core_specs import SpecResolver
|
||||||
from redbot.core.utils.chat_formatting import box
|
from redbot.core.utils.chat_formatting import box
|
||||||
|
from redbot.core.utils.predicates import MessagePredicate
|
||||||
|
|
||||||
_ = Translator("DataConverter", __file__)
|
_ = Translator("DataConverter", __file__)
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class DataConverter:
|
class DataConverter(commands.Cog):
|
||||||
"""
|
"""Import Red V2 data to your V3 instance."""
|
||||||
Cog for importing Red v2 Data
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@commands.command(name="convertdata")
|
@commands.command(name="convertdata")
|
||||||
async def dataconversioncommand(self, ctx: commands.Context, v2path: str):
|
async def dataconversioncommand(self, ctx: commands.Context, v2path: str):
|
||||||
"""
|
"""Interactive prompt for importing data from Red V2.
|
||||||
Interactive prompt for importing data from Red v2
|
|
||||||
|
|
||||||
Takes the path where the v2 install is
|
Takes the path where the V2 install is, and overwrites
|
||||||
|
values which have entries in both V2 and v3; use with caution.
|
||||||
Overwrites values which have entries in both v2 and v3,
|
|
||||||
use with caution.
|
|
||||||
"""
|
"""
|
||||||
resolver = SpecResolver(Path(v2path.strip()))
|
resolver = SpecResolver(Path(v2path.strip()))
|
||||||
|
|
||||||
@@ -47,13 +44,12 @@ class DataConverter:
|
|||||||
|
|
||||||
menu_message = await ctx.send(box(menu))
|
menu_message = await ctx.send(box(menu))
|
||||||
|
|
||||||
def pred(m):
|
|
||||||
return m.channel == ctx.channel and m.author == ctx.author
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = await self.bot.wait_for("message", check=pred, timeout=60)
|
message = await self.bot.wait_for(
|
||||||
|
"message", check=MessagePredicate.same_context(ctx), timeout=60
|
||||||
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
return await ctx.send(_("Try this again when you are more ready"))
|
return await ctx.send(_("Try this again when you are ready."))
|
||||||
else:
|
else:
|
||||||
if message.content.strip().lower() in ["quit", "exit", "-1", "q", "cancel"]:
|
if message.content.strip().lower() in ["quit", "exit", "-1", "q", "cancel"]:
|
||||||
return await ctx.tick()
|
return await ctx.tick()
|
||||||
@@ -71,7 +67,7 @@ class DataConverter:
|
|||||||
else:
|
else:
|
||||||
return await ctx.send(
|
return await ctx.send(
|
||||||
_(
|
_(
|
||||||
"There isn't anything else I know how to convert here."
|
"There isn't anything else I know how to convert here.\n"
|
||||||
"\nThere might be more things I can convert in the future."
|
"There might be more things I can convert in the future."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from redbot.core.bot import Red
|
|
||||||
from .downloader import Downloader
|
from .downloader import Downloader
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Red):
|
async def setup(bot):
|
||||||
bot.add_cog(Downloader(bot))
|
cog = Downloader(bot)
|
||||||
|
await cog.initialize()
|
||||||
|
bot.add_cog(cog)
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import discord
|
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
|
from redbot.core.i18n import Translator
|
||||||
|
from redbot.core.utils.predicates import MessagePredicate
|
||||||
|
|
||||||
__all__ = ["do_install_agreement"]
|
__all__ = ["do_install_agreement"]
|
||||||
|
|
||||||
REPO_INSTALL_MSG = (
|
T_ = Translator("DownloaderChecks", __file__)
|
||||||
|
|
||||||
|
_ = lambda s: s
|
||||||
|
REPO_INSTALL_MSG = _(
|
||||||
"You're about to add a 3rd party repository. The creator of Red"
|
"You're about to add a 3rd party repository. The creator of Red"
|
||||||
" and its community have no responsibility for any potential "
|
" and its community have no responsibility for any potential "
|
||||||
"damage that the content of 3rd party repositories might cause."
|
"damage that the content of 3rd party repositories might cause."
|
||||||
@@ -14,6 +18,7 @@ REPO_INSTALL_MSG = (
|
|||||||
"shown again until the next reboot.\n\nYou have **30** seconds"
|
"shown again until the next reboot.\n\nYou have **30** seconds"
|
||||||
" to reply to this message."
|
" to reply to this message."
|
||||||
)
|
)
|
||||||
|
_ = T_
|
||||||
|
|
||||||
|
|
||||||
async def do_install_agreement(ctx: commands.Context):
|
async def do_install_agreement(ctx: commands.Context):
|
||||||
@@ -21,15 +26,14 @@ async def do_install_agreement(ctx: commands.Context):
|
|||||||
if downloader is None or downloader.already_agreed:
|
if downloader is None or downloader.already_agreed:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def does_agree(msg: discord.Message):
|
await ctx.send(T_(REPO_INSTALL_MSG))
|
||||||
return ctx.author == msg.author and ctx.channel == msg.channel and msg.content == "I agree"
|
|
||||||
|
|
||||||
await ctx.send(REPO_INSTALL_MSG)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await ctx.bot.wait_for("message", check=does_agree, timeout=30)
|
await ctx.bot.wait_for(
|
||||||
|
"message", check=MessagePredicate.lower_equal_to("i agree", ctx), timeout=30
|
||||||
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send("Your response has timed out, please try again.")
|
await ctx.send(_("Your response has timed out, please try again."))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
downloader.already_agreed = True
|
downloader.already_agreed = True
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ from redbot.core import commands
|
|||||||
from .installable import Installable
|
from .installable import Installable
|
||||||
|
|
||||||
|
|
||||||
class InstalledCog(commands.Converter):
|
class InstalledCog(Installable):
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> Installable:
|
@classmethod
|
||||||
|
async def convert(cls, ctx: commands.Context, arg: str) -> Installable:
|
||||||
downloader = ctx.bot.get_cog("Downloader")
|
downloader = ctx.bot.get_cog("Downloader")
|
||||||
if downloader is None:
|
if downloader is None:
|
||||||
raise commands.CommandError("Downloader not loaded.")
|
raise commands.CommandError(_("No Downloader cog found."))
|
||||||
|
|
||||||
cog = discord.utils.get(await downloader.installed_cogs(), name=arg)
|
cog = discord.utils.get(await downloader.installed_cogs(), name=arg)
|
||||||
if cog is None:
|
if cog is None:
|
||||||
raise commands.BadArgument("That cog is not installed")
|
raise commands.BadArgument(_("That cog is not installed"))
|
||||||
|
|
||||||
return cog
|
return cog
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
|
import asyncio
|
||||||
|
import contextlib
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from sys import path as syspath
|
from sys import path as syspath
|
||||||
from typing import Tuple, Union
|
from typing import Tuple, Union, Iterable
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
import sys
|
from redbot.core import checks, commands, Config
|
||||||
|
from redbot.core.bot import Red
|
||||||
from redbot.core import Config
|
|
||||||
from redbot.core import checks
|
|
||||||
from redbot.core.data_manager import cog_data_path
|
from redbot.core.data_manager import cog_data_path
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.chat_formatting import box, pagify
|
from redbot.core.utils.chat_formatting import box, pagify, humanize_list, inline
|
||||||
from redbot.core import commands
|
from redbot.core.utils.menus import start_adding_reactions
|
||||||
|
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
|
||||||
|
|
||||||
from redbot.core.bot import Red
|
from . import errors
|
||||||
from .checks import do_install_agreement
|
from .checks import do_install_agreement
|
||||||
from .converters import InstalledCog
|
from .converters import InstalledCog
|
||||||
from .errors import CloningError, ExistingGitRepo
|
|
||||||
from .installable import Installable
|
from .installable import Installable
|
||||||
from .log import log
|
from .log import log
|
||||||
from .repo_manager import RepoManager, Repo
|
from .repo_manager import RepoManager, Repo
|
||||||
@@ -26,8 +27,9 @@ _ = Translator("Downloader", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Downloader:
|
class Downloader(commands.Cog):
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
self.conf = Config.get_conf(self, identifier=998240343, force_registration=True)
|
self.conf = Config.get_conf(self, identifier=998240343, force_registration=True)
|
||||||
@@ -51,6 +53,9 @@ class Downloader:
|
|||||||
|
|
||||||
self._repo_manager = RepoManager()
|
self._repo_manager = RepoManager()
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
await self._repo_manager.initialize()
|
||||||
|
|
||||||
async def cog_install_path(self):
|
async def cog_install_path(self):
|
||||||
"""Get the current cog install path.
|
"""Get the current cog install path.
|
||||||
|
|
||||||
@@ -107,7 +112,7 @@ class Downloader:
|
|||||||
installed.remove(cog_json)
|
installed.remove(cog_json)
|
||||||
await self.conf.installed.set(installed)
|
await self.conf.installed.set(installed)
|
||||||
|
|
||||||
async def _reinstall_cogs(self, cogs: Tuple[Installable]) -> Tuple[Installable]:
|
async def _reinstall_cogs(self, cogs: Iterable[Installable]) -> Tuple[Installable]:
|
||||||
"""
|
"""
|
||||||
Installs a list of cogs, used when updating.
|
Installs a list of cogs, used when updating.
|
||||||
:param cogs:
|
:param cogs:
|
||||||
@@ -121,7 +126,7 @@ class Downloader:
|
|||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return tuple(failed)
|
return tuple(failed)
|
||||||
|
|
||||||
async def _reinstall_libraries(self, cogs: Tuple[Installable]) -> Tuple[Installable]:
|
async def _reinstall_libraries(self, cogs: Iterable[Installable]) -> Tuple[Installable]:
|
||||||
"""
|
"""
|
||||||
Reinstalls any shared libraries from the repos of cogs that
|
Reinstalls any shared libraries from the repos of cogs that
|
||||||
were updated.
|
were updated.
|
||||||
@@ -141,7 +146,7 @@ class Downloader:
|
|||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return tuple(failed)
|
return tuple(failed)
|
||||||
|
|
||||||
async def _reinstall_requirements(self, cogs: Tuple[Installable]) -> bool:
|
async def _reinstall_requirements(self, cogs: Iterable[Installable]) -> bool:
|
||||||
"""
|
"""
|
||||||
Reinstalls requirements for given cogs that have been updated.
|
Reinstalls requirements for given cogs that have been updated.
|
||||||
Returns a bool that indicates if all requirement installations
|
Returns a bool that indicates if all requirement installations
|
||||||
@@ -188,9 +193,7 @@ class Downloader:
|
|||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def pipinstall(self, ctx, *deps: str):
|
async def pipinstall(self, ctx, *deps: str):
|
||||||
"""
|
"""Install a group of dependencies using pip."""
|
||||||
Installs a group of dependencies using pip.
|
|
||||||
"""
|
|
||||||
repo = Repo("", "", "", Path.cwd(), loop=ctx.bot.loop)
|
repo = Repo("", "", "", Path.cwd(), loop=ctx.bot.loop)
|
||||||
success = await repo.install_raw_requirements(deps, self.LIB_PATH)
|
success = await repo.install_raw_requirements(deps, self.LIB_PATH)
|
||||||
|
|
||||||
@@ -207,18 +210,15 @@ class Downloader:
|
|||||||
@commands.group()
|
@commands.group()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def repo(self, ctx):
|
async def repo(self, ctx):
|
||||||
"""
|
"""Repo management commands."""
|
||||||
Command group for managing Downloader repos.
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@repo.command(name="add")
|
@repo.command(name="add")
|
||||||
async def _repo_add(self, ctx, name: str, repo_url: str, branch: str = None):
|
async def _repo_add(self, ctx, name: str, repo_url: str, branch: str = None):
|
||||||
"""
|
"""Add a new repo.
|
||||||
Add a new repo to Downloader.
|
|
||||||
|
|
||||||
Name can only contain characters A-z, numbers and underscore
|
The name can only contain characters A-z, numbers and underscores.
|
||||||
Branch will default to master if not specified
|
The branch will be the default branch if not specified.
|
||||||
"""
|
"""
|
||||||
agreed = await do_install_agreement(ctx)
|
agreed = await do_install_agreement(ctx)
|
||||||
if not agreed:
|
if not agreed:
|
||||||
@@ -226,30 +226,33 @@ class Downloader:
|
|||||||
try:
|
try:
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
repo = await self._repo_manager.add_repo(name=name, url=repo_url, branch=branch)
|
repo = await self._repo_manager.add_repo(name=name, url=repo_url, branch=branch)
|
||||||
except ExistingGitRepo:
|
except errors.ExistingGitRepo:
|
||||||
await ctx.send(_("That git repo has already been added under another name."))
|
await ctx.send(_("That git repo has already been added under another name."))
|
||||||
except CloningError:
|
except errors.CloningError as err:
|
||||||
await ctx.send(_("Something went wrong during the cloning process."))
|
await ctx.send(_("Something went wrong during the cloning process."))
|
||||||
log.exception(_("Something went wrong during the cloning process."))
|
log.exception(
|
||||||
|
"Something went wrong whilst cloning %s (to revision: %s)",
|
||||||
|
repo_url,
|
||||||
|
branch,
|
||||||
|
exc_info=err,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Repo `{}` successfully added.").format(name))
|
await ctx.send(_("Repo `{name}` successfully added.").format(name=name))
|
||||||
if repo.install_msg is not None:
|
if repo.install_msg is not None:
|
||||||
await ctx.send(repo.install_msg.replace("[p]", ctx.prefix))
|
await ctx.send(repo.install_msg.replace("[p]", ctx.prefix))
|
||||||
|
|
||||||
@repo.command(name="delete")
|
@repo.command(name="delete", aliases=["remove"], usage="<repo_name>")
|
||||||
async def _repo_del(self, ctx, repo_name: Repo):
|
async def _repo_del(self, ctx, repo: Repo):
|
||||||
"""
|
"""Remove a repo and its files."""
|
||||||
Removes a repo from Downloader and its' files.
|
await self._repo_manager.delete_repo(repo.name)
|
||||||
"""
|
|
||||||
await self._repo_manager.delete_repo(repo_name.name)
|
|
||||||
|
|
||||||
await ctx.send(_("The repo `{}` has been deleted successfully.").format(repo_name.name))
|
await ctx.send(
|
||||||
|
_("The repo `{repo.name}` has been deleted successfully.").format(repo=repo)
|
||||||
|
)
|
||||||
|
|
||||||
@repo.command(name="list")
|
@repo.command(name="list")
|
||||||
async def _repo_list(self, ctx):
|
async def _repo_list(self, ctx):
|
||||||
"""
|
"""List all installed repos."""
|
||||||
Lists all installed repos.
|
|
||||||
"""
|
|
||||||
repos = self._repo_manager.get_all_repo_names()
|
repos = self._repo_manager.get_all_repo_names()
|
||||||
repos = sorted(repos, key=str.lower)
|
repos = sorted(repos, key=str.lower)
|
||||||
joined = _("Installed Repos:\n\n")
|
joined = _("Installed Repos:\n\n")
|
||||||
@@ -260,124 +263,156 @@ class Downloader:
|
|||||||
for page in pagify(joined, ["\n"], shorten_by=16):
|
for page in pagify(joined, ["\n"], shorten_by=16):
|
||||||
await ctx.send(box(page.lstrip(" "), lang="diff"))
|
await ctx.send(box(page.lstrip(" "), lang="diff"))
|
||||||
|
|
||||||
@repo.command(name="info")
|
@repo.command(name="info", usage="<repo_name>")
|
||||||
async def _repo_info(self, ctx, repo_name: Repo):
|
async def _repo_info(self, ctx, repo: Repo):
|
||||||
"""
|
"""Show information about a repo."""
|
||||||
Lists information about a single repo
|
if repo is None:
|
||||||
"""
|
await ctx.send(_("Repo `{repo.name}` not found.").format(repo=repo))
|
||||||
if repo_name is None:
|
|
||||||
await ctx.send(_("There is no repo `{}`").format(repo_name.name))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
msg = _("Information on {}:\n{}").format(repo_name.name, repo_name.description or "")
|
msg = _("Information on {repo.name}:\n{description}").format(
|
||||||
|
repo=repo, description=repo.description or ""
|
||||||
|
)
|
||||||
await ctx.send(box(msg))
|
await ctx.send(box(msg))
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def cog(self, ctx):
|
async def cog(self, ctx):
|
||||||
"""
|
"""Cog installation management commands."""
|
||||||
Command group for managing installable Cogs.
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@cog.command(name="install")
|
@cog.command(name="install", usage="<repo_name> <cog_name>")
|
||||||
async def _cog_install(self, ctx, repo_name: Repo, cog_name: str):
|
async def _cog_install(self, ctx, repo: Repo, cog_name: str):
|
||||||
"""
|
"""Install a cog from the given repo."""
|
||||||
Installs a cog from the given repo.
|
cog: Installable = discord.utils.get(repo.available_cogs, name=cog_name)
|
||||||
"""
|
|
||||||
cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable
|
|
||||||
if cog is None:
|
if cog is None:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Error, there is no cog by the name of `{}` in the `{}` repo.").format(
|
_(
|
||||||
cog_name, repo_name.name
|
"Error: there is no cog by the name of `{cog_name}` in the `{repo.name}` repo."
|
||||||
)
|
).format(cog_name=cog_name, repo=repo)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
elif cog.min_python_version > sys.version_info:
|
elif cog.min_python_version > sys.version_info:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("This cog requires at least python version {}, aborting install.").format(
|
_("This cog requires at least python version {version}, aborting install.").format(
|
||||||
".".join([str(n) for n in cog.min_python_version])
|
version=".".join([str(n) for n in cog.min_python_version])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not await repo_name.install_requirements(cog, self.LIB_PATH):
|
if not await repo.install_requirements(cog, self.LIB_PATH):
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Failed to install the required libraries for `{}`: `{}`").format(
|
_(
|
||||||
cog.name, cog.requirements
|
"Failed to install the required libraries for `{cog_name}`: `{libraries}`"
|
||||||
)
|
).format(cog_name=cog.name, libraries=cog.requirements)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
await repo_name.install_cog(cog, await self.cog_install_path())
|
await repo.install_cog(cog, await self.cog_install_path())
|
||||||
|
|
||||||
await self._add_to_installed(cog)
|
await self._add_to_installed(cog)
|
||||||
|
|
||||||
await repo_name.install_libraries(self.SHAREDLIB_PATH)
|
await repo.install_libraries(self.SHAREDLIB_PATH)
|
||||||
|
|
||||||
await ctx.send(_("`{}` cog successfully installed.").format(cog_name))
|
await ctx.send(_("Cog `{cog_name}` successfully installed.").format(cog_name=cog_name))
|
||||||
if cog.install_msg is not None:
|
if cog.install_msg is not None:
|
||||||
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
|
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
|
||||||
|
|
||||||
@cog.command(name="uninstall")
|
@cog.command(name="uninstall", usage="<cog_name>")
|
||||||
async def _cog_uninstall(self, ctx, cog_name: InstalledCog):
|
async def _cog_uninstall(self, ctx, cog: InstalledCog):
|
||||||
"""
|
"""Uninstall a cog.
|
||||||
Allows you to uninstall cogs that were previously installed
|
|
||||||
through Downloader.
|
You may only uninstall cogs which were previously installed
|
||||||
|
by Downloader.
|
||||||
"""
|
"""
|
||||||
# noinspection PyUnresolvedReferences,PyProtectedMember
|
# noinspection PyUnresolvedReferences,PyProtectedMember
|
||||||
real_name = cog_name.name
|
real_name = cog.name
|
||||||
|
|
||||||
poss_installed_path = (await self.cog_install_path()) / real_name
|
poss_installed_path = (await self.cog_install_path()) / real_name
|
||||||
if poss_installed_path.exists():
|
if poss_installed_path.exists():
|
||||||
await self._delete_cog(poss_installed_path)
|
await self._delete_cog(poss_installed_path)
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
await self._remove_from_installed(cog_name)
|
await self._remove_from_installed(cog)
|
||||||
await ctx.send(_("`{}` was successfully removed.").format(real_name))
|
await ctx.send(
|
||||||
|
_("Cog `{cog_name}` was successfully uninstalled.").format(cog_name=real_name)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"That cog was installed but can no longer"
|
"That cog was installed but can no longer"
|
||||||
" be located. You may need to remove it's"
|
" be located. You may need to remove it's"
|
||||||
" files manually if it is still usable."
|
" files manually if it is still usable."
|
||||||
)
|
" Also make sure you've unloaded the cog"
|
||||||
|
" with `{prefix}unload {cog_name}`."
|
||||||
|
).format(cog_name=real_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
@cog.command(name="update")
|
@cog.command(name="update")
|
||||||
async def _cog_update(self, ctx, cog_name: InstalledCog = None):
|
async def _cog_update(self, ctx, cog_name: InstalledCog = None):
|
||||||
"""
|
"""Update all cogs, or one of your choosing."""
|
||||||
Updates all cogs or one of your choosing.
|
|
||||||
"""
|
|
||||||
installed_cogs = set(await self.installed_cogs())
|
installed_cogs = set(await self.installed_cogs())
|
||||||
|
|
||||||
if cog_name is None:
|
async with ctx.typing():
|
||||||
updated = await self._repo_manager.update_all_repos()
|
if cog_name is None:
|
||||||
|
updated = await self._repo_manager.update_all_repos()
|
||||||
|
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
updated = await self._repo_manager.update_repo(cog_name.repo_name)
|
||||||
|
except KeyError:
|
||||||
|
# Thrown if the repo no longer exists
|
||||||
|
updated = {}
|
||||||
|
|
||||||
|
updated_cogs = set(cog for repo in updated for cog in repo.available_cogs)
|
||||||
|
installed_and_updated = updated_cogs & installed_cogs
|
||||||
|
|
||||||
|
if installed_and_updated:
|
||||||
|
await self._reinstall_requirements(installed_and_updated)
|
||||||
|
await self._reinstall_cogs(installed_and_updated)
|
||||||
|
await self._reinstall_libraries(installed_and_updated)
|
||||||
|
message = _("Cog update completed successfully.")
|
||||||
|
|
||||||
|
cognames = [c.name for c in installed_and_updated]
|
||||||
|
message += _("\nUpdated: ") + humanize_list(tuple(map(inline, cognames)))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("All installed cogs are already up to date."))
|
||||||
|
return
|
||||||
|
await ctx.send(message)
|
||||||
|
|
||||||
|
message = _("Would you like to reload the updated cogs?")
|
||||||
|
can_react = ctx.channel.permissions_for(ctx.me).add_reactions
|
||||||
|
if not can_react:
|
||||||
|
message += " (y/n)"
|
||||||
|
query: discord.Message = await ctx.send(message)
|
||||||
|
if can_react:
|
||||||
|
# noinspection PyAsyncCall
|
||||||
|
start_adding_reactions(query, ReactionPredicate.YES_OR_NO_EMOJIS, ctx.bot.loop)
|
||||||
|
pred = ReactionPredicate.yes_or_no(query, ctx.author)
|
||||||
|
event = "reaction_add"
|
||||||
else:
|
else:
|
||||||
try:
|
pred = MessagePredicate.yes_or_no(ctx)
|
||||||
updated = await self._repo_manager.update_repo(cog_name.repo_name)
|
event = "message"
|
||||||
except KeyError:
|
try:
|
||||||
# Thrown if the repo no longer exists
|
await ctx.bot.wait_for(event, check=pred, timeout=30)
|
||||||
updated = {}
|
except asyncio.TimeoutError:
|
||||||
|
await query.delete()
|
||||||
|
return
|
||||||
|
|
||||||
updated_cogs = set(cog for repo in updated.keys() for cog in repo.available_cogs)
|
if pred.result is True:
|
||||||
installed_and_updated = updated_cogs & installed_cogs
|
if can_react:
|
||||||
|
with contextlib.suppress(discord.Forbidden):
|
||||||
|
await query.clear_reactions()
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
await ctx.invoke(ctx.bot.get_cog("Core").reload, *cognames)
|
||||||
await self._reinstall_requirements(installed_and_updated)
|
else:
|
||||||
|
if can_react:
|
||||||
|
await query.delete()
|
||||||
|
else:
|
||||||
|
await ctx.send(_("OK then."))
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
@cog.command(name="list", usage="<repo_name>")
|
||||||
await self._reinstall_cogs(installed_and_updated)
|
async def _cog_list(self, ctx, repo: Repo):
|
||||||
|
"""List all available cogs from a single repo."""
|
||||||
# noinspection PyTypeChecker
|
|
||||||
await self._reinstall_libraries(installed_and_updated)
|
|
||||||
await ctx.send(_("Cog update completed successfully."))
|
|
||||||
|
|
||||||
@cog.command(name="list")
|
|
||||||
async def _cog_list(self, ctx, repo_name: Repo):
|
|
||||||
"""
|
|
||||||
Lists all available cogs from a single repo.
|
|
||||||
"""
|
|
||||||
installed = await self.installed_cogs()
|
installed = await self.installed_cogs()
|
||||||
installed_str = ""
|
installed_str = ""
|
||||||
if installed:
|
if installed:
|
||||||
@@ -385,10 +420,10 @@ class Downloader:
|
|||||||
[
|
[
|
||||||
"- {}{}".format(i.name, ": {}".format(i.short) if i.short else "")
|
"- {}{}".format(i.name, ": {}".format(i.short) if i.short else "")
|
||||||
for i in installed
|
for i in installed
|
||||||
if i.repo_name == repo_name.name
|
if i.repo_name == repo.name
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
cogs = repo_name.available_cogs
|
cogs = repo.available_cogs
|
||||||
cogs = _("Available Cogs:\n") + "\n".join(
|
cogs = _("Available Cogs:\n") + "\n".join(
|
||||||
[
|
[
|
||||||
"+ {}: {}".format(c.name, c.short or "")
|
"+ {}: {}".format(c.name, c.short or "")
|
||||||
@@ -400,20 +435,24 @@ class Downloader:
|
|||||||
for page in pagify(cogs, ["\n"], shorten_by=16):
|
for page in pagify(cogs, ["\n"], shorten_by=16):
|
||||||
await ctx.send(box(page.lstrip(" "), lang="diff"))
|
await ctx.send(box(page.lstrip(" "), lang="diff"))
|
||||||
|
|
||||||
@cog.command(name="info")
|
@cog.command(name="info", usage="<repo_name> <cog_name>")
|
||||||
async def _cog_info(self, ctx, repo_name: Repo, cog_name: str):
|
async def _cog_info(self, ctx, repo: Repo, cog_name: str):
|
||||||
"""
|
"""List information about a single cog."""
|
||||||
Lists information about a single cog.
|
cog = discord.utils.get(repo.available_cogs, name=cog_name)
|
||||||
"""
|
|
||||||
cog = discord.utils.get(repo_name.available_cogs, name=cog_name)
|
|
||||||
if cog is None:
|
if cog is None:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("There is no cog `{}` in the repo `{}`").format(cog_name, repo_name.name)
|
_("There is no cog `{cog_name}` in the repo `{repo.name}`").format(
|
||||||
|
cog_name=cog_name, repo=repo
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
msg = _("Information on {}:\n{}\n\nRequirements: {}").format(
|
msg = _(
|
||||||
cog.name, cog.description or "", ", ".join(cog.requirements) or "None"
|
"Information on {cog_name}:\n{description}\n\nRequirements: {requirements}"
|
||||||
|
).format(
|
||||||
|
cog_name=cog.name,
|
||||||
|
description=cog.description or "",
|
||||||
|
requirements=", ".join(cog.requirements) or "None",
|
||||||
)
|
)
|
||||||
await ctx.send(box(msg))
|
await ctx.send(box(msg))
|
||||||
|
|
||||||
@@ -467,9 +506,9 @@ class Downloader:
|
|||||||
repo_url = "https://github.com/Cog-Creators/Red-DiscordBot"
|
repo_url = "https://github.com/Cog-Creators/Red-DiscordBot"
|
||||||
cog_name = cog_installable.__class__.__name__
|
cog_name = cog_installable.__class__.__name__
|
||||||
|
|
||||||
msg = _("Command: {}\nMade by: {}\nRepo: {}\nCog name: {}")
|
msg = _("Command: {command}\nMade by: {author}\nRepo: {repo}\nCog name: {cog}")
|
||||||
|
|
||||||
return msg.format(command_name, made_by, repo_url, cog_name)
|
return msg.format(command=command_name, author=made_by, repo=repo_url, cog=cog_name)
|
||||||
|
|
||||||
def cog_name_from_instance(self, instance: object) -> str:
|
def cog_name_from_instance(self, instance: object) -> str:
|
||||||
"""Determines the cog name that Downloader knows from the cog instance.
|
"""Determines the cog name that Downloader knows from the cog instance.
|
||||||
@@ -492,9 +531,9 @@ class Downloader:
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def findcog(self, ctx: commands.Context, command_name: str):
|
async def findcog(self, ctx: commands.Context, command_name: str):
|
||||||
"""
|
"""Find which cog a command comes from.
|
||||||
Figures out which cog a command comes from. Only works with loaded
|
|
||||||
cogs.
|
This will only work with loaded cogs.
|
||||||
"""
|
"""
|
||||||
command = ctx.bot.all_commands.get(command_name)
|
command = ctx.bot.all_commands.get(command_name)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ __all__ = [
|
|||||||
"HardResetError",
|
"HardResetError",
|
||||||
"UpdateError",
|
"UpdateError",
|
||||||
"GitDiffError",
|
"GitDiffError",
|
||||||
|
"NoRemoteURL",
|
||||||
"PipError",
|
"PipError",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -96,6 +97,14 @@ class GitDiffError(GitException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoRemoteURL(GitException):
|
||||||
|
"""
|
||||||
|
Thrown when no remote URL exists for a repo.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PipError(DownloaderException):
|
class PipError(DownloaderException):
|
||||||
"""
|
"""
|
||||||
Thrown when pip returns a non-zero return code.
|
Thrown when pip returns a non-zero return code.
|
||||||
|
|||||||
@@ -2,27 +2,33 @@ import asyncio
|
|||||||
import functools
|
import functools
|
||||||
import os
|
import os
|
||||||
import pkgutil
|
import pkgutil
|
||||||
|
import shutil
|
||||||
|
import re
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import run as sp_run, PIPE
|
from subprocess import run as sp_run, PIPE
|
||||||
from sys import executable
|
from sys import executable
|
||||||
from typing import Tuple, MutableMapping, Union
|
from typing import Tuple, MutableMapping, Union, Optional
|
||||||
|
|
||||||
from redbot.core import data_manager, commands
|
from redbot.core import data_manager, commands
|
||||||
from redbot.core.utils import safe_delete
|
from redbot.core.utils import safe_delete
|
||||||
from .errors import *
|
from redbot.core.i18n import Translator
|
||||||
|
|
||||||
|
from . import errors
|
||||||
from .installable import Installable, InstallableType
|
from .installable import Installable, InstallableType
|
||||||
from .json_mixins import RepoJSONMixin
|
from .json_mixins import RepoJSONMixin
|
||||||
from .log import log
|
from .log import log
|
||||||
|
|
||||||
|
_ = Translator("RepoManager", __file__)
|
||||||
|
|
||||||
|
|
||||||
class Repo(RepoJSONMixin):
|
class Repo(RepoJSONMixin):
|
||||||
GIT_CLONE = "git clone -b {branch} {url} {folder}"
|
GIT_CLONE = "git clone --recurse-submodules -b {branch} {url} {folder}"
|
||||||
GIT_CLONE_NO_BRANCH = "git clone {url} {folder}"
|
GIT_CLONE_NO_BRANCH = "git clone --recurse-submodules {url} {folder}"
|
||||||
GIT_CURRENT_BRANCH = "git -C {path} rev-parse --abbrev-ref HEAD"
|
GIT_CURRENT_BRANCH = "git -C {path} rev-parse --abbrev-ref HEAD"
|
||||||
GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}"
|
GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}"
|
||||||
GIT_HARD_RESET = "git -C {path} reset --hard origin/{branch} -q"
|
GIT_HARD_RESET = "git -C {path} reset --hard origin/{branch} -q"
|
||||||
GIT_PULL = "git -C {path} pull -q --ff-only"
|
GIT_PULL = "git -C {path} pull --recurse-submodules -q --ff-only"
|
||||||
GIT_DIFF_FILE_STATUS = "git -C {path} diff --no-commit-id --name-status {old_hash} {new_hash}"
|
GIT_DIFF_FILE_STATUS = "git -C {path} diff --no-commit-id --name-status {old_hash} {new_hash}"
|
||||||
GIT_LOG = "git -C {path} log --relative-date --reverse {old_hash}.. {relative_file_path}"
|
GIT_LOG = "git -C {path} log --relative-date --reverse {old_hash}.. {relative_file_path}"
|
||||||
GIT_DISCOVER_REMOTE_URL = "git -C {path} config --get remote.origin.url"
|
GIT_DISCOVER_REMOTE_URL = "git -C {path} config --get remote.origin.url"
|
||||||
@@ -62,13 +68,15 @@ class Repo(RepoJSONMixin):
|
|||||||
async def convert(cls, ctx: commands.Context, argument: str):
|
async def convert(cls, ctx: commands.Context, argument: str):
|
||||||
downloader_cog = ctx.bot.get_cog("Downloader")
|
downloader_cog = ctx.bot.get_cog("Downloader")
|
||||||
if downloader_cog is None:
|
if downloader_cog is None:
|
||||||
raise commands.CommandError("No Downloader cog found.")
|
raise commands.CommandError(_("No Downloader cog found."))
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
repo_manager = downloader_cog._repo_manager
|
repo_manager = downloader_cog._repo_manager
|
||||||
poss_repo = repo_manager.get_repo(argument)
|
poss_repo = repo_manager.get_repo(argument)
|
||||||
if poss_repo is None:
|
if poss_repo is None:
|
||||||
raise commands.BadArgument("Repo by the name {} does not exist.".format(argument))
|
raise commands.BadArgument(
|
||||||
|
_('Repo by the name "{repo_name}" does not exist.').format(repo_name=argument)
|
||||||
|
)
|
||||||
return poss_repo
|
return poss_repo
|
||||||
|
|
||||||
def _existing_git_repo(self) -> (bool, Path):
|
def _existing_git_repo(self) -> (bool, Path):
|
||||||
@@ -92,7 +100,9 @@ class Repo(RepoJSONMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise GitDiffError("Git diff failed for repo at path: {}".format(self.folder_path))
|
raise errors.GitDiffError(
|
||||||
|
"Git diff failed for repo at path: {}".format(self.folder_path)
|
||||||
|
)
|
||||||
|
|
||||||
stdout = p.stdout.strip().decode().split("\n")
|
stdout = p.stdout.strip().decode().split("\n")
|
||||||
|
|
||||||
@@ -122,7 +132,7 @@ class Repo(RepoJSONMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise GitException(
|
raise errors.GitException(
|
||||||
"An exception occurred while executing git log on"
|
"An exception occurred while executing git log on"
|
||||||
" this repo: {}".format(self.folder_path)
|
" this repo: {}".format(self.folder_path)
|
||||||
)
|
)
|
||||||
@@ -176,7 +186,7 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
exists, path = self._existing_git_repo()
|
exists, path = self._existing_git_repo()
|
||||||
if exists:
|
if exists:
|
||||||
raise ExistingGitRepo("A git repo already exists at path: {}".format(path))
|
raise errors.ExistingGitRepo("A git repo already exists at path: {}".format(path))
|
||||||
|
|
||||||
if self.branch is not None:
|
if self.branch is not None:
|
||||||
p = await self._run(
|
p = await self._run(
|
||||||
@@ -189,8 +199,10 @@ class Repo(RepoJSONMixin):
|
|||||||
self.GIT_CLONE_NO_BRANCH.format(url=self.url, folder=self.folder_path).split()
|
self.GIT_CLONE_NO_BRANCH.format(url=self.url, folder=self.folder_path).split()
|
||||||
)
|
)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode:
|
||||||
raise CloningError("Error when running git clone.")
|
# Try cleaning up folder
|
||||||
|
shutil.rmtree(str(self.folder_path), ignore_errors=True)
|
||||||
|
raise errors.CloningError("Error when running git clone.")
|
||||||
|
|
||||||
if self.branch is None:
|
if self.branch is None:
|
||||||
self.branch = await self.current_branch()
|
self.branch = await self.current_branch()
|
||||||
@@ -210,12 +222,14 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
exists, _ = self._existing_git_repo()
|
exists, _ = self._existing_git_repo()
|
||||||
if not exists:
|
if not exists:
|
||||||
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
|
raise errors.MissingGitRepo(
|
||||||
|
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||||
|
)
|
||||||
|
|
||||||
p = await self._run(self.GIT_CURRENT_BRANCH.format(path=self.folder_path).split())
|
p = await self._run(self.GIT_CURRENT_BRANCH.format(path=self.folder_path).split())
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise GitException(
|
raise errors.GitException(
|
||||||
"Could not determine current branch at path: {}".format(self.folder_path)
|
"Could not determine current branch at path: {}".format(self.folder_path)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -240,14 +254,16 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
exists, _ = self._existing_git_repo()
|
exists, _ = self._existing_git_repo()
|
||||||
if not exists:
|
if not exists:
|
||||||
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
|
raise errors.MissingGitRepo(
|
||||||
|
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||||
|
)
|
||||||
|
|
||||||
p = await self._run(
|
p = await self._run(
|
||||||
self.GIT_LATEST_COMMIT.format(path=self.folder_path, branch=branch).split()
|
self.GIT_LATEST_COMMIT.format(path=self.folder_path, branch=branch).split()
|
||||||
)
|
)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise CurrentHashError("Unable to determine old commit hash.")
|
raise errors.CurrentHashError("Unable to determine old commit hash.")
|
||||||
|
|
||||||
return p.stdout.decode().strip()
|
return p.stdout.decode().strip()
|
||||||
|
|
||||||
@@ -267,8 +283,9 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
RuntimeError
|
.NoRemoteURL
|
||||||
When the folder does not contain a git repo with a FETCH URL.
|
When the folder does not contain a git repo with a FETCH URL.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if folder is None:
|
if folder is None:
|
||||||
folder = self.folder_path
|
folder = self.folder_path
|
||||||
@@ -276,7 +293,7 @@ class Repo(RepoJSONMixin):
|
|||||||
p = await self._run(Repo.GIT_DISCOVER_REMOTE_URL.format(path=folder).split())
|
p = await self._run(Repo.GIT_DISCOVER_REMOTE_URL.format(path=folder).split())
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise RuntimeError("Unable to discover a repo URL.")
|
raise errors.NoRemoteURL("Unable to discover a repo URL.")
|
||||||
|
|
||||||
return p.stdout.decode().strip()
|
return p.stdout.decode().strip()
|
||||||
|
|
||||||
@@ -294,14 +311,16 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
exists, _ = self._existing_git_repo()
|
exists, _ = self._existing_git_repo()
|
||||||
if not exists:
|
if not exists:
|
||||||
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
|
raise errors.MissingGitRepo(
|
||||||
|
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||||
|
)
|
||||||
|
|
||||||
p = await self._run(
|
p = await self._run(
|
||||||
self.GIT_HARD_RESET.format(path=self.folder_path, branch=branch).split()
|
self.GIT_HARD_RESET.format(path=self.folder_path, branch=branch).split()
|
||||||
)
|
)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise HardResetError(
|
raise errors.HardResetError(
|
||||||
"Some error occurred when trying to"
|
"Some error occurred when trying to"
|
||||||
" execute a hard reset on the repo at"
|
" execute a hard reset on the repo at"
|
||||||
" the following path: {}".format(self.folder_path)
|
" the following path: {}".format(self.folder_path)
|
||||||
@@ -324,7 +343,7 @@ class Repo(RepoJSONMixin):
|
|||||||
p = await self._run(self.GIT_PULL.format(path=self.folder_path).split())
|
p = await self._run(self.GIT_PULL.format(path=self.folder_path).split())
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise UpdateError(
|
raise errors.UpdateError(
|
||||||
"Git pull returned a non zero exit code"
|
"Git pull returned a non zero exit code"
|
||||||
" for the repo located at path: {}".format(self.folder_path)
|
" for the repo located at path: {}".format(self.folder_path)
|
||||||
)
|
)
|
||||||
@@ -353,7 +372,7 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if cog not in self.available_cogs:
|
if cog not in self.available_cogs:
|
||||||
raise DownloaderException("That cog does not exist in this repo")
|
raise errors.DownloaderException("That cog does not exist in this repo")
|
||||||
|
|
||||||
if not target_dir.is_dir():
|
if not target_dir.is_dir():
|
||||||
raise ValueError("That target directory is not actually a directory.")
|
raise ValueError("That target directory is not actually a directory.")
|
||||||
@@ -489,13 +508,19 @@ class Repo(RepoJSONMixin):
|
|||||||
|
|
||||||
|
|
||||||
class RepoManager:
|
class RepoManager:
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
|
GITHUB_OR_GITLAB_RE = re.compile("https?://git(?:hub)|(?:lab)\.com/")
|
||||||
|
TREE_URL_RE = re.compile(r"(?P<tree>/tree)/(?P<branch>\S+)$")
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
self._repos = {}
|
self._repos = {}
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.create_task(self._load_repos(set=True)) # str_name: Repo
|
loop.create_task(self._load_repos(set=True)) # str_name: Repo
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
await self._load_repos(set=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def repos_folder(self) -> Path:
|
def repos_folder(self) -> Path:
|
||||||
data_folder = data_manager.cog_data_path(self)
|
data_folder = data_manager.cog_data_path(self)
|
||||||
@@ -507,10 +532,10 @@ class RepoManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_and_normalize_repo_name(name: str) -> str:
|
def validate_and_normalize_repo_name(name: str) -> str:
|
||||||
if not name.isidentifier():
|
if not name.isidentifier():
|
||||||
raise InvalidRepoName("Not a valid Python variable name.")
|
raise errors.InvalidRepoName("Not a valid Python variable name.")
|
||||||
return name.lower()
|
return name.lower()
|
||||||
|
|
||||||
async def add_repo(self, url: str, name: str, branch: str = "master") -> Repo:
|
async def add_repo(self, url: str, name: str, branch: Optional[str] = None) -> Repo:
|
||||||
"""Add and clone a git repository.
|
"""Add and clone a git repository.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -529,10 +554,12 @@ class RepoManager:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if self.does_repo_exist(name):
|
if self.does_repo_exist(name):
|
||||||
raise ExistingGitRepo(
|
raise errors.ExistingGitRepo(
|
||||||
"That repo name you provided already exists. Please choose another."
|
"That repo name you provided already exists. Please choose another."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
url, branch = self._parse_url(url, branch)
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
r = Repo(url=url, name=name, branch=branch, folder_path=self.repos_folder / name)
|
r = Repo(url=url, name=name, branch=branch, folder_path=self.repos_folder / name)
|
||||||
await r.clone()
|
await r.clone()
|
||||||
@@ -578,13 +605,13 @@ class RepoManager:
|
|||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
MissingGitRepo
|
.MissingGitRepo
|
||||||
If the repo does not exist.
|
If the repo does not exist.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
repo = self.get_repo(name)
|
repo = self.get_repo(name)
|
||||||
if repo is None:
|
if repo is None:
|
||||||
raise MissingGitRepo("There is no repo with the name {}".format(name))
|
raise errors.MissingGitRepo("There is no repo with the name {}".format(name))
|
||||||
|
|
||||||
safe_delete(repo.folder_path)
|
safe_delete(repo.folder_path)
|
||||||
|
|
||||||
@@ -623,10 +650,26 @@ class RepoManager:
|
|||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
ret[folder.stem] = await Repo.from_folder(folder)
|
ret[folder.stem] = await Repo.from_folder(folder)
|
||||||
except RuntimeError:
|
except errors.NoRemoteURL:
|
||||||
# Thrown when there's no findable git remote URL
|
log.warning("A remote URL does not exist for repo %s", folder.stem)
|
||||||
pass
|
except errors.DownloaderException as err:
|
||||||
|
log.error("Discarding repo %s due to error.", folder.stem, exc_info=err)
|
||||||
|
shutil.rmtree(
|
||||||
|
str(folder),
|
||||||
|
onerror=lambda func, path, exc: log.error(
|
||||||
|
"Failed to remove folder %s", path, exc_info=exc
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if set:
|
if set:
|
||||||
self._repos = ret
|
self._repos = ret
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def _parse_url(self, url: str, branch: Optional[str]) -> Tuple[str, Optional[str]]:
|
||||||
|
if self.GITHUB_OR_GITLAB_RE.match(url):
|
||||||
|
tree_url_match = self.TREE_URL_RE.search(url)
|
||||||
|
if tree_url_match:
|
||||||
|
url = url[: tree_url_match.start("tree")]
|
||||||
|
if branch is None:
|
||||||
|
branch = tree_url_match["branch"]
|
||||||
|
return url, branch
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import logging
|
|||||||
import random
|
import random
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import cast, Iterable
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
|||||||
|
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
_ = Translator("Economy", __file__)
|
T_ = Translator("Economy", __file__)
|
||||||
|
|
||||||
logger = logging.getLogger("red.economy")
|
logger = logging.getLogger("red.economy")
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ class SMReel(Enum):
|
|||||||
snowflake = "\N{SNOWFLAKE}"
|
snowflake = "\N{SNOWFLAKE}"
|
||||||
|
|
||||||
|
|
||||||
|
_ = lambda s: s
|
||||||
PAYOUTS = {
|
PAYOUTS = {
|
||||||
(SMReel.two, SMReel.two, SMReel.six): {
|
(SMReel.two, SMReel.two, SMReel.six): {
|
||||||
"payout": lambda x: x * 2500 + x,
|
"payout": lambda x: x * 2500 + x,
|
||||||
@@ -72,6 +74,7 @@ SLOT_PAYOUTS_MSG = _(
|
|||||||
"Three symbols: +500\n"
|
"Three symbols: +500\n"
|
||||||
"Two symbols: Bet * 2"
|
"Two symbols: Bet * 2"
|
||||||
).format(**SMReel.__dict__)
|
).format(**SMReel.__dict__)
|
||||||
|
_ = T_
|
||||||
|
|
||||||
|
|
||||||
def guild_only_check():
|
def guild_only_check():
|
||||||
@@ -105,10 +108,8 @@ class SetParser:
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Economy:
|
class Economy(commands.Cog):
|
||||||
"""Economy
|
"""Get rich and have fun with imaginary currency!"""
|
||||||
|
|
||||||
Get rich and have fun with imaginary currency!"""
|
|
||||||
|
|
||||||
default_guild_settings = {
|
default_guild_settings = {
|
||||||
"PAYDAY_TIME": 300,
|
"PAYDAY_TIME": 300,
|
||||||
@@ -128,6 +129,7 @@ class Economy:
|
|||||||
default_user_settings = default_member_settings
|
default_user_settings = default_member_settings
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.file_path = "data/economy/settings.json"
|
self.file_path = "data/economy/settings.json"
|
||||||
self.config = Config.get_conf(self, 1256844281)
|
self.config = Config.get_conf(self, 1256844281)
|
||||||
@@ -141,12 +143,12 @@ class Economy:
|
|||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
@commands.group(name="bank")
|
@commands.group(name="bank")
|
||||||
async def _bank(self, ctx: commands.Context):
|
async def _bank(self, ctx: commands.Context):
|
||||||
"""Bank operations"""
|
"""Manage the bank."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@_bank.command()
|
@_bank.command()
|
||||||
async def balance(self, ctx: commands.Context, user: discord.Member = None):
|
async def balance(self, ctx: commands.Context, user: discord.Member = None):
|
||||||
"""Shows balance of user.
|
"""Show the user's account balance.
|
||||||
|
|
||||||
Defaults to yours."""
|
Defaults to yours."""
|
||||||
if user is None:
|
if user is None:
|
||||||
@@ -155,11 +157,15 @@ class Economy:
|
|||||||
bal = await bank.get_balance(user)
|
bal = await bank.get_balance(user)
|
||||||
currency = await bank.get_currency_name(ctx.guild)
|
currency = await bank.get_currency_name(ctx.guild)
|
||||||
|
|
||||||
await ctx.send(_("{}'s balance is {} {}").format(user.display_name, bal, currency))
|
await ctx.send(
|
||||||
|
_("{user}'s balance is {num} {currency}").format(
|
||||||
|
user=user.display_name, num=bal, currency=currency
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@_bank.command()
|
@_bank.command()
|
||||||
async def transfer(self, ctx: commands.Context, to: discord.Member, amount: int):
|
async def transfer(self, ctx: commands.Context, to: discord.Member, amount: int):
|
||||||
"""Transfer currency to other users"""
|
"""Transfer currency to other users."""
|
||||||
from_ = ctx.author
|
from_ = ctx.author
|
||||||
currency = await bank.get_currency_name(ctx.guild)
|
currency = await bank.get_currency_name(ctx.guild)
|
||||||
|
|
||||||
@@ -169,72 +175,83 @@ class Economy:
|
|||||||
return await ctx.send(str(e))
|
return await ctx.send(str(e))
|
||||||
|
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} transferred {} {} to {}").format(
|
_("{user} transferred {num} {currency} to {other_user}").format(
|
||||||
from_.display_name, amount, currency, to.display_name
|
user=from_.display_name, num=amount, currency=currency, other_user=to.display_name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@_bank.command(name="set")
|
@_bank.command(name="set")
|
||||||
@check_global_setting_admin()
|
@check_global_setting_admin()
|
||||||
async def _set(self, ctx: commands.Context, to: discord.Member, creds: SetParser):
|
async def _set(self, ctx: commands.Context, to: discord.Member, creds: SetParser):
|
||||||
"""Sets balance of user's bank account. See help for more operations
|
"""Set the balance of user's bank account.
|
||||||
|
|
||||||
Passing positive and negative values will add/remove currency instead
|
Passing positive and negative values will add/remove currency instead.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
bank set @Twentysix 26 - Sets balance to 26
|
- `[p]bank set @Twentysix 26` - Sets balance to 26
|
||||||
bank set @Twentysix +2 - Increases balance by 2
|
- `[p]bank set @Twentysix +2` - Increases balance by 2
|
||||||
bank set @Twentysix -6 - Decreases balance by 6"""
|
- `[p]bank set @Twentysix -6` - Decreases balance by 6
|
||||||
|
"""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
currency = await bank.get_currency_name(ctx.guild)
|
currency = await bank.get_currency_name(ctx.guild)
|
||||||
|
|
||||||
if creds.operation == "deposit":
|
if creds.operation == "deposit":
|
||||||
await bank.deposit_credits(to, creds.sum)
|
await bank.deposit_credits(to, creds.sum)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} added {} {} to {}'s account.").format(
|
_("{author} added {num} {currency} to {user}'s account.").format(
|
||||||
author.display_name, creds.sum, currency, to.display_name
|
author=author.display_name,
|
||||||
|
num=creds.sum,
|
||||||
|
currency=currency,
|
||||||
|
user=to.display_name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif creds.operation == "withdraw":
|
elif creds.operation == "withdraw":
|
||||||
await bank.withdraw_credits(to, creds.sum)
|
await bank.withdraw_credits(to, creds.sum)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} removed {} {} from {}'s account.").format(
|
_("{author} removed {num} {currency} from {user}'s account.").format(
|
||||||
author.display_name, creds.sum, currency, to.display_name
|
author=author.display_name,
|
||||||
|
num=creds.sum,
|
||||||
|
currency=currency,
|
||||||
|
user=to.display_name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await bank.set_balance(to, creds.sum)
|
await bank.set_balance(to, creds.sum)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} set {}'s account to {} {}.").format(
|
_("{author} set {users}'s account balance to {num} {currency}.").format(
|
||||||
author.display_name, to.display_name, creds.sum, currency
|
author=author.display_name,
|
||||||
|
num=creds.sum,
|
||||||
|
currency=currency,
|
||||||
|
user=to.display_name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@_bank.command()
|
@_bank.command()
|
||||||
@check_global_setting_guildowner()
|
@check_global_setting_guildowner()
|
||||||
async def reset(self, ctx, confirmation: bool = False):
|
async def reset(self, ctx, confirmation: bool = False):
|
||||||
"""Deletes bank accounts"""
|
"""Delete all bank accounts."""
|
||||||
if confirmation is False:
|
if confirmation is False:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"This will delete all bank accounts for {}.\nIf you're sure, type "
|
"This will delete all bank accounts for {scope}.\nIf you're sure, type "
|
||||||
"`{}bank reset yes`"
|
"`{prefix}bank reset yes`"
|
||||||
).format(
|
).format(
|
||||||
self.bot.user.name if await bank.is_global() else "this server", ctx.prefix
|
scope=self.bot.user.name if await bank.is_global() else _("this server"),
|
||||||
|
prefix=ctx.prefix,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await bank.wipe_bank()
|
await bank.wipe_bank(guild=ctx.guild)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("All bank accounts for {} have been deleted.").format(
|
_("All bank accounts for {scope} have been deleted.").format(
|
||||||
self.bot.user.name if await bank.is_global() else "this server"
|
scope=self.bot.user.name if await bank.is_global() else _("this server")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def payday(self, ctx: commands.Context):
|
async def payday(self, ctx: commands.Context):
|
||||||
"""Get some free currency"""
|
"""Get some free currency."""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
|
||||||
@@ -250,24 +267,25 @@ class Economy:
|
|||||||
pos = await bank.get_leaderboard_position(author)
|
pos = await bank.get_leaderboard_position(author)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
"{author.mention} Here, take some {currency}. "
|
||||||
"You currently have {3} {1}.\n\n"
|
"Enjoy! (+{amount} {new_balance}!)\n\n"
|
||||||
"You are currently #{4} on the global leaderboard!"
|
"You currently have {new_balance} {currency}.\n\n"
|
||||||
|
"You are currently #{pos} on the global leaderboard!"
|
||||||
).format(
|
).format(
|
||||||
author,
|
author=author,
|
||||||
credits_name,
|
currency=credits_name,
|
||||||
str(await self.config.PAYDAY_CREDITS()),
|
amount=await self.config.PAYDAY_CREDITS(),
|
||||||
str(await bank.get_balance(author)),
|
new_balance=await bank.get_balance(author),
|
||||||
pos,
|
pos=pos,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
dtime = self.display_time(next_payday - cur_time)
|
dtime = self.display_time(next_payday - cur_time)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} Too soon. For your next payday you have to wait {}.").format(
|
_(
|
||||||
author.mention, dtime
|
"{author.mention} Too soon. For your next payday you have to wait {time}."
|
||||||
)
|
).format(author=author, time=dtime)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
next_payday = await self.config.member(author).next_payday()
|
next_payday = await self.config.member(author).next_payday()
|
||||||
@@ -285,31 +303,33 @@ class Economy:
|
|||||||
pos = await bank.get_leaderboard_position(author)
|
pos = await bank.get_leaderboard_position(author)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
"{author.mention} Here, take some {currency}. "
|
||||||
"You currently have {3} {1}.\n\n"
|
"Enjoy! (+{amount} {new_balance}!)\n\n"
|
||||||
"You are currently #{4} on the leaderboard!"
|
"You currently have {new_balance} {currency}.\n\n"
|
||||||
|
"You are currently #{pos} on the global leaderboard!"
|
||||||
).format(
|
).format(
|
||||||
author,
|
author=author,
|
||||||
credits_name,
|
currency=credits_name,
|
||||||
credit_amount,
|
amount=credit_amount,
|
||||||
str(await bank.get_balance(author)),
|
new_balance=await bank.get_balance(author),
|
||||||
pos,
|
pos=pos,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
dtime = self.display_time(next_payday - cur_time)
|
dtime = self.display_time(next_payday - cur_time)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} Too soon. For your next payday you have to wait {}.").format(
|
_(
|
||||||
author.mention, dtime
|
"{author.mention} Too soon. For your next payday you have to wait {time}."
|
||||||
)
|
).format(author=author, time=dtime)
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
async def leaderboard(self, ctx: commands.Context, top: int = 10, show_global: bool = False):
|
async def leaderboard(self, ctx: commands.Context, top: int = 10, show_global: bool = False):
|
||||||
"""Prints out the leaderboard
|
"""Print the leaderboard.
|
||||||
|
|
||||||
Defaults to top 10"""
|
Defaults to top 10.
|
||||||
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
if top < 1:
|
if top < 1:
|
||||||
@@ -319,9 +339,9 @@ class Economy:
|
|||||||
): # show_global is only applicable if bank is global
|
): # show_global is only applicable if bank is global
|
||||||
guild = None
|
guild = None
|
||||||
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
||||||
if len(bank_sorted) < top:
|
header = "{pound:4}{name:36}{score:2}\n".format(
|
||||||
top = len(bank_sorted)
|
pound="#", name=_("Name"), score=_("Score")
|
||||||
header = f"{f'#':4}{f'Name':36}{f'Score':2}\n"
|
)
|
||||||
highscores = [
|
highscores = [
|
||||||
(
|
(
|
||||||
f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} "
|
f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} "
|
||||||
@@ -346,13 +366,13 @@ class Economy:
|
|||||||
@commands.command()
|
@commands.command()
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
async def payouts(self, ctx: commands.Context):
|
async def payouts(self, ctx: commands.Context):
|
||||||
"""Shows slot machine payouts"""
|
"""Show the payouts for the slot machine."""
|
||||||
await ctx.author.send(SLOT_PAYOUTS_MSG)
|
await ctx.author.send(SLOT_PAYOUTS_MSG())
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
async def slot(self, ctx: commands.Context, bid: int):
|
async def slot(self, ctx: commands.Context, bid: int):
|
||||||
"""Play the slot machine"""
|
"""Use the slot machine."""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
@@ -385,8 +405,9 @@ class Economy:
|
|||||||
await self.config.member(author).last_slot.set(now)
|
await self.config.member(author).last_slot.set(now)
|
||||||
await self.slot_machine(author, channel, bid)
|
await self.slot_machine(author, channel, bid)
|
||||||
|
|
||||||
async def slot_machine(self, author, channel, bid):
|
@staticmethod
|
||||||
default_reel = deque(SMReel)
|
async def slot_machine(author, channel, bid):
|
||||||
|
default_reel = deque(cast(Iterable, SMReel))
|
||||||
reels = []
|
reels = []
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
default_reel.rotate(random.randint(-999, 999)) # weeeeee
|
default_reel.rotate(random.randint(-999, 999)) # weeeeee
|
||||||
@@ -424,60 +445,62 @@ class Economy:
|
|||||||
pay = payout["payout"](bid)
|
pay = payout["payout"](bid)
|
||||||
now = then - bid + pay
|
now = then - bid + pay
|
||||||
await bank.set_balance(author, now)
|
await bank.set_balance(author, now)
|
||||||
await channel.send(
|
phrase = T_(payout["phrase"])
|
||||||
_("{}\n{} {}\n\nYour bid: {}\n{} → {}!").format(
|
|
||||||
slot, author.mention, payout["phrase"], bid, then, now
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
then = await bank.get_balance(author)
|
then = await bank.get_balance(author)
|
||||||
await bank.withdraw_credits(author, bid)
|
await bank.withdraw_credits(author, bid)
|
||||||
now = then - bid
|
now = then - bid
|
||||||
await channel.send(
|
phrase = _("Nothing!")
|
||||||
_("{}\n{} Nothing!\nYour bid: {}\n{} → {}!").format(
|
await channel.send(
|
||||||
slot, author.mention, bid, then, now
|
(
|
||||||
)
|
"{slot}\n{author.mention} {phrase}\n\n"
|
||||||
|
+ _("Your bid: {amount}")
|
||||||
|
+ "\n{old_balance} → {new_balance}!"
|
||||||
|
).format(
|
||||||
|
slot=slot,
|
||||||
|
author=author,
|
||||||
|
phrase=phrase,
|
||||||
|
amount=bid,
|
||||||
|
old_balance=then,
|
||||||
|
new_balance=now,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
@check_global_setting_admin()
|
@check_global_setting_admin()
|
||||||
async def economyset(self, ctx: commands.Context):
|
async def economyset(self, ctx: commands.Context):
|
||||||
"""Changes economy module settings"""
|
"""Manage Economy settings."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
if await bank.is_global():
|
if await bank.is_global():
|
||||||
slot_min = await self.config.SLOT_MIN()
|
conf = self.config
|
||||||
slot_max = await self.config.SLOT_MAX()
|
|
||||||
slot_time = await self.config.SLOT_TIME()
|
|
||||||
payday_time = await self.config.PAYDAY_TIME()
|
|
||||||
payday_amount = await self.config.PAYDAY_CREDITS()
|
|
||||||
else:
|
else:
|
||||||
slot_min = await self.config.guild(guild).SLOT_MIN()
|
conf = self.config.guild(ctx.guild)
|
||||||
slot_max = await self.config.guild(guild).SLOT_MAX()
|
await ctx.send(
|
||||||
slot_time = await self.config.guild(guild).SLOT_TIME()
|
box(
|
||||||
payday_time = await self.config.guild(guild).PAYDAY_TIME()
|
_(
|
||||||
payday_amount = await self.config.guild(guild).PAYDAY_CREDITS()
|
"----Economy Settings---\n"
|
||||||
register_amount = await bank.get_default_balance(guild)
|
"Minimum slot bid: {slot_min}\n"
|
||||||
msg = box(
|
"Maximum slot bid: {slot_max}\n"
|
||||||
_(
|
"Slot cooldown: {slot_time}\n"
|
||||||
"Minimum slot bid: {}\n"
|
"Payday amount: {payday_amount}\n"
|
||||||
"Maximum slot bid: {}\n"
|
"Payday cooldown: {payday_time}\n"
|
||||||
"Slot cooldown: {}\n"
|
"Amount given at account registration: {register_amount}"
|
||||||
"Payday amount: {}\n"
|
).format(
|
||||||
"Payday cooldown: {}\n"
|
slot_min=await conf.SLOT_MIN(),
|
||||||
"Amount given at account registration: {}"
|
slot_max=await conf.SLOT_MAX(),
|
||||||
""
|
slot_time=await conf.SLOT_TIME(),
|
||||||
).format(
|
payday_time=await conf.PAYDAY_TIME(),
|
||||||
slot_min, slot_max, slot_time, payday_amount, payday_time, register_amount
|
payday_amount=await conf.PAYDAY_CREDITS(),
|
||||||
),
|
register_amount=await bank.get_default_balance(guild),
|
||||||
_("Current Economy settings:"),
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
await ctx.send(msg)
|
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def slotmin(self, ctx: commands.Context, bid: int):
|
async def slotmin(self, ctx: commands.Context, bid: int):
|
||||||
"""Minimum slot machine bid"""
|
"""Set the minimum slot machine bid."""
|
||||||
if bid < 1:
|
if bid < 1:
|
||||||
await ctx.send(_("Invalid min bid amount."))
|
await ctx.send(_("Invalid min bid amount."))
|
||||||
return
|
return
|
||||||
@@ -487,14 +510,18 @@ class Economy:
|
|||||||
else:
|
else:
|
||||||
await self.config.guild(guild).SLOT_MIN.set(bid)
|
await self.config.guild(guild).SLOT_MIN.set(bid)
|
||||||
credits_name = await bank.get_currency_name(guild)
|
credits_name = await bank.get_currency_name(guild)
|
||||||
await ctx.send(_("Minimum bid is now {} {}.").format(bid, credits_name))
|
await ctx.send(
|
||||||
|
_("Minimum bid is now {bid} {currency}.").format(bid=bid, currency=credits_name)
|
||||||
|
)
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def slotmax(self, ctx: commands.Context, bid: int):
|
async def slotmax(self, ctx: commands.Context, bid: int):
|
||||||
"""Maximum slot machine bid"""
|
"""Set the maximum slot machine bid."""
|
||||||
slot_min = await self.config.SLOT_MIN()
|
slot_min = await self.config.SLOT_MIN()
|
||||||
if bid < 1 or bid < slot_min:
|
if bid < 1 or bid < slot_min:
|
||||||
await ctx.send(_("Invalid slotmax bid amount. Must be greater than slotmin."))
|
await ctx.send(
|
||||||
|
_("Invalid maximum bid amount. Must be greater than the minimum amount.")
|
||||||
|
)
|
||||||
return
|
return
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
credits_name = await bank.get_currency_name(guild)
|
credits_name = await bank.get_currency_name(guild)
|
||||||
@@ -502,33 +529,37 @@ class Economy:
|
|||||||
await self.config.SLOT_MAX.set(bid)
|
await self.config.SLOT_MAX.set(bid)
|
||||||
else:
|
else:
|
||||||
await self.config.guild(guild).SLOT_MAX.set(bid)
|
await self.config.guild(guild).SLOT_MAX.set(bid)
|
||||||
await ctx.send(_("Maximum bid is now {} {}.").format(bid, credits_name))
|
await ctx.send(
|
||||||
|
_("Maximum bid is now {bid} {currency}.").format(bid=bid, currency=credits_name)
|
||||||
|
)
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def slottime(self, ctx: commands.Context, seconds: int):
|
async def slottime(self, ctx: commands.Context, seconds: int):
|
||||||
"""Seconds between each slots use"""
|
"""Set the cooldown for the slot machine."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if await bank.is_global():
|
if await bank.is_global():
|
||||||
await self.config.SLOT_TIME.set(seconds)
|
await self.config.SLOT_TIME.set(seconds)
|
||||||
else:
|
else:
|
||||||
await self.config.guild(guild).SLOT_TIME.set(seconds)
|
await self.config.guild(guild).SLOT_TIME.set(seconds)
|
||||||
await ctx.send(_("Cooldown is now {} seconds.").format(seconds))
|
await ctx.send(_("Cooldown is now {num} seconds.").format(num=seconds))
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def paydaytime(self, ctx: commands.Context, seconds: int):
|
async def paydaytime(self, ctx: commands.Context, seconds: int):
|
||||||
"""Seconds between each payday"""
|
"""Set the cooldown for payday."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if await bank.is_global():
|
if await bank.is_global():
|
||||||
await self.config.PAYDAY_TIME.set(seconds)
|
await self.config.PAYDAY_TIME.set(seconds)
|
||||||
else:
|
else:
|
||||||
await self.config.guild(guild).PAYDAY_TIME.set(seconds)
|
await self.config.guild(guild).PAYDAY_TIME.set(seconds)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Value modified. At least {} seconds must pass between each payday.").format(seconds)
|
_("Value modified. At least {num} seconds must pass between each payday.").format(
|
||||||
|
num=seconds
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def paydayamount(self, ctx: commands.Context, creds: int):
|
async def paydayamount(self, ctx: commands.Context, creds: int):
|
||||||
"""Amount earned each payday"""
|
"""Set the amount earned each payday."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
credits_name = await bank.get_currency_name(guild)
|
credits_name = await bank.get_currency_name(guild)
|
||||||
if creds <= 0:
|
if creds <= 0:
|
||||||
@@ -538,37 +569,45 @@ class Economy:
|
|||||||
await self.config.PAYDAY_CREDITS.set(creds)
|
await self.config.PAYDAY_CREDITS.set(creds)
|
||||||
else:
|
else:
|
||||||
await self.config.guild(guild).PAYDAY_CREDITS.set(creds)
|
await self.config.guild(guild).PAYDAY_CREDITS.set(creds)
|
||||||
await ctx.send(_("Every payday will now give {} {}.").format(creds, credits_name))
|
await ctx.send(
|
||||||
|
_("Every payday will now give {num} {currency}.").format(
|
||||||
|
num=creds, currency=credits_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
|
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
|
||||||
"""Amount earned each payday for a role"""
|
"""Set the amount earned each payday for a role."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
credits_name = await bank.get_currency_name(guild)
|
credits_name = await bank.get_currency_name(guild)
|
||||||
if await bank.is_global():
|
if await bank.is_global():
|
||||||
await ctx.send("The bank must be per-server for per-role paydays to work.")
|
await ctx.send(_("The bank must be per-server for per-role paydays to work."))
|
||||||
else:
|
else:
|
||||||
await self.config.role(role).PAYDAY_CREDITS.set(creds)
|
await self.config.role(role).PAYDAY_CREDITS.set(creds)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Every payday will now give {} {} to people with the role {}.").format(
|
_(
|
||||||
creds, credits_name, role.name
|
"Every payday will now give {num} {currency} "
|
||||||
)
|
"to people with the role {role_name}."
|
||||||
|
).format(num=creds, currency=credits_name, role_name=role.name)
|
||||||
)
|
)
|
||||||
|
|
||||||
@economyset.command()
|
@economyset.command()
|
||||||
async def registeramount(self, ctx: commands.Context, creds: int):
|
async def registeramount(self, ctx: commands.Context, creds: int):
|
||||||
"""Amount given on registering an account"""
|
"""Set the initial balance for new bank accounts."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if creds < 0:
|
if creds < 0:
|
||||||
creds = 0
|
creds = 0
|
||||||
credits_name = await bank.get_currency_name(guild)
|
credits_name = await bank.get_currency_name(guild)
|
||||||
await bank.set_default_balance(creds, guild)
|
await bank.set_default_balance(creds, guild)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Registering an account will now give {} {}.").format(creds, credits_name)
|
_("Registering an account will now give {num} {currency}.").format(
|
||||||
|
num=creds, currency=credits_name
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# What would I ever do without stackoverflow?
|
# What would I ever do without stackoverflow?
|
||||||
def display_time(self, seconds, granularity=2):
|
@staticmethod
|
||||||
|
def display_time(seconds, granularity=2):
|
||||||
intervals = ( # Source: http://stackoverflow.com/a/24542445
|
intervals = ( # Source: http://stackoverflow.com/a/24542445
|
||||||
(_("weeks"), 604800), # 60 * 60 * 24 * 7
|
(_("weeks"), 604800), # 60 * 60 * 24 * 7
|
||||||
(_("days"), 86400), # 60 * 60 * 24
|
(_("days"), 86400), # 60 * 60 * 24
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import discord
|
import discord
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from redbot.core import checks, Config, modlog, commands
|
from redbot.core import checks, Config, modlog, commands
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.chat_formatting import pagify
|
from redbot.core.utils.chat_formatting import pagify
|
||||||
from redbot.core.utils.mod import is_mod_or_superior
|
|
||||||
|
|
||||||
_ = Translator("Filter", __file__)
|
_ = Translator("Filter", __file__)
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Filter:
|
class Filter(commands.Cog):
|
||||||
"""Filter-related commands"""
|
"""Filter unwanted words and phrases from text channels."""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.settings = Config.get_conf(self, 4766951341)
|
self.settings = Config.get_conf(self, 4766951341)
|
||||||
default_guild_settings = {
|
default_guild_settings = {
|
||||||
@@ -24,14 +25,17 @@ class Filter:
|
|||||||
"filter_default_name": "John Doe",
|
"filter_default_name": "John Doe",
|
||||||
}
|
}
|
||||||
default_member_settings = {"filter_count": 0, "next_reset_time": 0}
|
default_member_settings = {"filter_count": 0, "next_reset_time": 0}
|
||||||
|
default_channel_settings = {"filter": []}
|
||||||
self.settings.register_guild(**default_guild_settings)
|
self.settings.register_guild(**default_guild_settings)
|
||||||
self.settings.register_member(**default_member_settings)
|
self.settings.register_member(**default_member_settings)
|
||||||
|
self.settings.register_channel(**default_channel_settings)
|
||||||
self.register_task = self.bot.loop.create_task(self.register_filterban())
|
self.register_task = self.bot.loop.create_task(self.register_filterban())
|
||||||
|
|
||||||
def __unload(self):
|
def __unload(self):
|
||||||
self.register_task.cancel()
|
self.register_task.cancel()
|
||||||
|
|
||||||
async def register_filterban(self):
|
@staticmethod
|
||||||
|
async def register_filterban():
|
||||||
try:
|
try:
|
||||||
await modlog.register_casetype(
|
await modlog.register_casetype(
|
||||||
"filterban", False, ":filing_cabinet: :hammer:", "Filter ban", "ban"
|
"filterban", False, ":filing_cabinet: :hammer:", "Filter ban", "ban"
|
||||||
@@ -39,119 +43,34 @@ class Filter:
|
|||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@commands.group(name="filter")
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.mod_or_permissions(manage_messages=True)
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
async def _filter(self, ctx: commands.Context):
|
async def filterset(self, ctx: commands.Context):
|
||||||
"""Adds/removes words from filter
|
"""Manage filter settings."""
|
||||||
|
pass
|
||||||
|
|
||||||
Use double quotes to add/remove sentences
|
@filterset.command(name="defaultname")
|
||||||
Using this command with no subcommands will send
|
|
||||||
the list of the server's filtered words."""
|
|
||||||
if ctx.invoked_subcommand is None:
|
|
||||||
server = ctx.guild
|
|
||||||
author = ctx.author
|
|
||||||
word_list = await self.settings.guild(server).filter()
|
|
||||||
if word_list:
|
|
||||||
words = ", ".join(word_list)
|
|
||||||
words = _("Filtered in this server:") + "\n\n" + words
|
|
||||||
try:
|
|
||||||
for page in pagify(words, delims=[" ", "\n"], shorten_by=8):
|
|
||||||
await author.send(page)
|
|
||||||
except discord.Forbidden:
|
|
||||||
await ctx.send(_("I can't send direct messages to you."))
|
|
||||||
|
|
||||||
@_filter.command(name="add")
|
|
||||||
async def filter_add(self, ctx: commands.Context, *, words: str):
|
|
||||||
"""Adds words to the filter
|
|
||||||
|
|
||||||
Use double quotes to add sentences
|
|
||||||
Examples:
|
|
||||||
filter add word1 word2 word3
|
|
||||||
filter add \"This is a sentence\""""
|
|
||||||
server = ctx.guild
|
|
||||||
split_words = words.split()
|
|
||||||
word_list = []
|
|
||||||
tmp = ""
|
|
||||||
for word in split_words:
|
|
||||||
if not word.startswith('"') and not word.endswith('"') and not tmp:
|
|
||||||
word_list.append(word)
|
|
||||||
else:
|
|
||||||
if word.startswith('"'):
|
|
||||||
tmp += word[1:] + " "
|
|
||||||
elif word.endswith('"'):
|
|
||||||
tmp += word[:-1]
|
|
||||||
word_list.append(tmp)
|
|
||||||
tmp = ""
|
|
||||||
else:
|
|
||||||
tmp += word + " "
|
|
||||||
added = await self.add_to_filter(server, word_list)
|
|
||||||
if added:
|
|
||||||
await ctx.send(_("Words added to filter."))
|
|
||||||
else:
|
|
||||||
await ctx.send(_("Words already in the filter."))
|
|
||||||
|
|
||||||
@_filter.command(name="remove")
|
|
||||||
async def filter_remove(self, ctx: commands.Context, *, words: str):
|
|
||||||
"""Remove words from the filter
|
|
||||||
|
|
||||||
Use double quotes to remove sentences
|
|
||||||
Examples:
|
|
||||||
filter remove word1 word2 word3
|
|
||||||
filter remove \"This is a sentence\""""
|
|
||||||
server = ctx.guild
|
|
||||||
split_words = words.split()
|
|
||||||
word_list = []
|
|
||||||
tmp = ""
|
|
||||||
for word in split_words:
|
|
||||||
if not word.startswith('"') and not word.endswith('"') and not tmp:
|
|
||||||
word_list.append(word)
|
|
||||||
else:
|
|
||||||
if word.startswith('"'):
|
|
||||||
tmp += word[1:] + " "
|
|
||||||
elif word.endswith('"'):
|
|
||||||
tmp += word[:-1]
|
|
||||||
word_list.append(tmp)
|
|
||||||
tmp = ""
|
|
||||||
else:
|
|
||||||
tmp += word + " "
|
|
||||||
removed = await self.remove_from_filter(server, word_list)
|
|
||||||
if removed:
|
|
||||||
await ctx.send(_("Words removed from filter."))
|
|
||||||
else:
|
|
||||||
await ctx.send(_("Those words weren't in the filter."))
|
|
||||||
|
|
||||||
@_filter.command(name="names")
|
|
||||||
async def filter_names(self, ctx: commands.Context):
|
|
||||||
"""Toggles whether or not to check names and nicknames against the filter
|
|
||||||
|
|
||||||
This is disabled by default
|
|
||||||
"""
|
|
||||||
guild = ctx.guild
|
|
||||||
current_setting = await self.settings.guild(guild).filter_names()
|
|
||||||
await self.settings.guild(guild).filter_names.set(not current_setting)
|
|
||||||
if current_setting:
|
|
||||||
await ctx.send(_("Names and nicknames will no longer be checked against the filter."))
|
|
||||||
else:
|
|
||||||
await ctx.send(_("Names and nicknames will now be checked against the filter."))
|
|
||||||
|
|
||||||
@_filter.command(name="defaultname")
|
|
||||||
async def filter_default_name(self, ctx: commands.Context, name: str):
|
async def filter_default_name(self, ctx: commands.Context, name: str):
|
||||||
"""Sets the default name to use if filtering names is enabled
|
"""Set the nickname for users with a filtered name.
|
||||||
|
|
||||||
Note that this has no effect if filtering names is disabled
|
Note that this has no effect if filtering names is disabled
|
||||||
|
(to toggle, run `[p]filter names`).
|
||||||
|
|
||||||
The default name used is John Doe
|
The default name used is *John Doe*.
|
||||||
"""
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
await self.settings.guild(guild).filter_default_name.set(name)
|
await self.settings.guild(guild).filter_default_name.set(name)
|
||||||
await ctx.send(_("The name to use on filtered names has been set."))
|
await ctx.send(_("The name to use on filtered names has been set."))
|
||||||
|
|
||||||
@_filter.command(name="ban")
|
@filterset.command(name="ban")
|
||||||
async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int):
|
async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int):
|
||||||
"""Autobans if the specified number of messages are filtered in the timeframe
|
"""Set the filter's autoban conditions.
|
||||||
|
|
||||||
The timeframe is represented by seconds.
|
Users will be banned if they send `<count>` filtered words in
|
||||||
|
`<timeframe>` seconds.
|
||||||
|
|
||||||
|
Set both to zero to disable autoban.
|
||||||
"""
|
"""
|
||||||
if (count <= 0) != (timeframe <= 0):
|
if (count <= 0) != (timeframe <= 0):
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
@@ -170,30 +89,241 @@ class Filter:
|
|||||||
await self.settings.guild(ctx.guild).filterban_time.set(timeframe)
|
await self.settings.guild(ctx.guild).filterban_time.set(timeframe)
|
||||||
await ctx.send(_("Count and time have been set."))
|
await ctx.send(_("Count and time have been set."))
|
||||||
|
|
||||||
async def add_to_filter(self, server: discord.Guild, words: list) -> bool:
|
@commands.group(name="filter")
|
||||||
|
@commands.guild_only()
|
||||||
|
@checks.mod_or_permissions(manage_messages=True)
|
||||||
|
async def _filter(self, ctx: commands.Context):
|
||||||
|
"""Add or remove words from server filter.
|
||||||
|
|
||||||
|
Use double quotes to add or remove sentences.
|
||||||
|
|
||||||
|
Using this command with no subcommands will send the list of
|
||||||
|
the server's filtered words.
|
||||||
|
"""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
server = ctx.guild
|
||||||
|
author = ctx.author
|
||||||
|
word_list = await self.settings.guild(server).filter()
|
||||||
|
if word_list:
|
||||||
|
words = ", ".join(word_list)
|
||||||
|
words = _("Filtered in this server:") + "\n\n" + words
|
||||||
|
try:
|
||||||
|
for page in pagify(words, delims=[" ", "\n"], shorten_by=8):
|
||||||
|
await author.send(page)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send(_("I can't send direct messages to you."))
|
||||||
|
|
||||||
|
@_filter.group(name="channel")
|
||||||
|
async def _filter_channel(self, ctx: commands.Context):
|
||||||
|
"""Add or remove words from channel filter.
|
||||||
|
|
||||||
|
Use double quotes to add or remove sentences.
|
||||||
|
|
||||||
|
Using this command with no subcommands will send the list of
|
||||||
|
the channel's filtered words.
|
||||||
|
"""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
channel = ctx.channel
|
||||||
|
author = ctx.author
|
||||||
|
word_list = await self.settings.channel(channel).filter()
|
||||||
|
if word_list:
|
||||||
|
words = ", ".join(word_list)
|
||||||
|
words = _("Filtered in this channel:") + "\n\n" + words
|
||||||
|
try:
|
||||||
|
for page in pagify(words, delims=[" ", "\n"], shorten_by=8):
|
||||||
|
await author.send(page)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send(_("I can't send direct messages to you."))
|
||||||
|
|
||||||
|
@_filter_channel.command("add")
|
||||||
|
async def filter_channel_add(self, ctx: commands.Context, *, words: str):
|
||||||
|
"""Add words to the filter.
|
||||||
|
|
||||||
|
Use double quotes to add sentences.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `[p]filter channel add word1 word2 word3`
|
||||||
|
- `[p]filter channel add "This is a sentence"`
|
||||||
|
"""
|
||||||
|
channel = ctx.channel
|
||||||
|
split_words = words.split()
|
||||||
|
word_list = []
|
||||||
|
tmp = ""
|
||||||
|
for word in split_words:
|
||||||
|
if not word.startswith('"') and not word.endswith('"') and not tmp:
|
||||||
|
word_list.append(word)
|
||||||
|
else:
|
||||||
|
if word.startswith('"'):
|
||||||
|
tmp += word[1:] + " "
|
||||||
|
elif word.endswith('"'):
|
||||||
|
tmp += word[:-1]
|
||||||
|
word_list.append(tmp)
|
||||||
|
tmp = ""
|
||||||
|
else:
|
||||||
|
tmp += word + " "
|
||||||
|
added = await self.add_to_filter(channel, word_list)
|
||||||
|
if added:
|
||||||
|
await ctx.send(_("Words added to filter."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Words already in the filter."))
|
||||||
|
|
||||||
|
@_filter_channel.command("remove")
|
||||||
|
async def filter_channel_remove(self, ctx: commands.Context, *, words: str):
|
||||||
|
"""Remove words from the filter.
|
||||||
|
|
||||||
|
Use double quotes to remove sentences.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `[p]filter channel remove word1 word2 word3`
|
||||||
|
- `[p]filter channel remove "This is a sentence"`
|
||||||
|
"""
|
||||||
|
channel = ctx.channel
|
||||||
|
split_words = words.split()
|
||||||
|
word_list = []
|
||||||
|
tmp = ""
|
||||||
|
for word in split_words:
|
||||||
|
if not word.startswith('"') and not word.endswith('"') and not tmp:
|
||||||
|
word_list.append(word)
|
||||||
|
else:
|
||||||
|
if word.startswith('"'):
|
||||||
|
tmp += word[1:] + " "
|
||||||
|
elif word.endswith('"'):
|
||||||
|
tmp += word[:-1]
|
||||||
|
word_list.append(tmp)
|
||||||
|
tmp = ""
|
||||||
|
else:
|
||||||
|
tmp += word + " "
|
||||||
|
removed = await self.remove_from_filter(channel, word_list)
|
||||||
|
if removed:
|
||||||
|
await ctx.send(_("Words removed from filter."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Those words weren't in the filter."))
|
||||||
|
|
||||||
|
@_filter.command(name="add")
|
||||||
|
async def filter_add(self, ctx: commands.Context, *, words: str):
|
||||||
|
"""Add words to the filter.
|
||||||
|
|
||||||
|
Use double quotes to add sentences.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `[p]filter add word1 word2 word3`
|
||||||
|
- `[p]filter add "This is a sentence"`
|
||||||
|
"""
|
||||||
|
server = ctx.guild
|
||||||
|
split_words = words.split()
|
||||||
|
word_list = []
|
||||||
|
tmp = ""
|
||||||
|
for word in split_words:
|
||||||
|
if not word.startswith('"') and not word.endswith('"') and not tmp:
|
||||||
|
word_list.append(word)
|
||||||
|
else:
|
||||||
|
if word.startswith('"'):
|
||||||
|
tmp += word[1:] + " "
|
||||||
|
elif word.endswith('"'):
|
||||||
|
tmp += word[:-1]
|
||||||
|
word_list.append(tmp)
|
||||||
|
tmp = ""
|
||||||
|
else:
|
||||||
|
tmp += word + " "
|
||||||
|
added = await self.add_to_filter(server, word_list)
|
||||||
|
if added:
|
||||||
|
await ctx.send(_("Words successfully added to filter."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Those words were already in the filter."))
|
||||||
|
|
||||||
|
@_filter.command(name="remove")
|
||||||
|
async def filter_remove(self, ctx: commands.Context, *, words: str):
|
||||||
|
"""Remove words from the filter.
|
||||||
|
|
||||||
|
Use double quotes to remove sentences.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `[p]filter remove word1 word2 word3`
|
||||||
|
- `[p]filter remove "This is a sentence"`
|
||||||
|
"""
|
||||||
|
server = ctx.guild
|
||||||
|
split_words = words.split()
|
||||||
|
word_list = []
|
||||||
|
tmp = ""
|
||||||
|
for word in split_words:
|
||||||
|
if not word.startswith('"') and not word.endswith('"') and not tmp:
|
||||||
|
word_list.append(word)
|
||||||
|
else:
|
||||||
|
if word.startswith('"'):
|
||||||
|
tmp += word[1:] + " "
|
||||||
|
elif word.endswith('"'):
|
||||||
|
tmp += word[:-1]
|
||||||
|
word_list.append(tmp)
|
||||||
|
tmp = ""
|
||||||
|
else:
|
||||||
|
tmp += word + " "
|
||||||
|
removed = await self.remove_from_filter(server, word_list)
|
||||||
|
if removed:
|
||||||
|
await ctx.send(_("Words successfully removed from filter."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Those words weren't in the filter."))
|
||||||
|
|
||||||
|
@_filter.command(name="names")
|
||||||
|
async def filter_names(self, ctx: commands.Context):
|
||||||
|
"""Toggle name and nickname filtering.
|
||||||
|
|
||||||
|
This is disabled by default.
|
||||||
|
"""
|
||||||
|
guild = ctx.guild
|
||||||
|
current_setting = await self.settings.guild(guild).filter_names()
|
||||||
|
await self.settings.guild(guild).filter_names.set(not current_setting)
|
||||||
|
if current_setting:
|
||||||
|
await ctx.send(_("Names and nicknames will no longer be filtered."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Names and nicknames will now be filtered."))
|
||||||
|
|
||||||
|
async def add_to_filter(
|
||||||
|
self, server_or_channel: Union[discord.Guild, discord.TextChannel], words: list
|
||||||
|
) -> bool:
|
||||||
added = False
|
added = False
|
||||||
async with self.settings.guild(server).filter() as cur_list:
|
if isinstance(server_or_channel, discord.Guild):
|
||||||
for w in words:
|
async with self.settings.guild(server_or_channel).filter() as cur_list:
|
||||||
if w.lower() not in cur_list and w:
|
for w in words:
|
||||||
cur_list.append(w.lower())
|
if w.lower() not in cur_list and w:
|
||||||
added = True
|
cur_list.append(w.lower())
|
||||||
|
added = True
|
||||||
|
|
||||||
|
elif isinstance(server_or_channel, discord.TextChannel):
|
||||||
|
async with self.settings.channel(server_or_channel).filter() as cur_list:
|
||||||
|
for w in words:
|
||||||
|
if w.lower not in cur_list and w:
|
||||||
|
cur_list.append(w.lower())
|
||||||
|
added = True
|
||||||
|
|
||||||
return added
|
return added
|
||||||
|
|
||||||
async def remove_from_filter(self, server: discord.Guild, words: list) -> bool:
|
async def remove_from_filter(
|
||||||
|
self, server_or_channel: Union[discord.Guild, discord.TextChannel], words: list
|
||||||
|
) -> bool:
|
||||||
removed = False
|
removed = False
|
||||||
async with self.settings.guild(server).filter() as cur_list:
|
if isinstance(server_or_channel, discord.Guild):
|
||||||
for w in words:
|
async with self.settings.guild(server_or_channel).filter() as cur_list:
|
||||||
if w.lower() in cur_list:
|
for w in words:
|
||||||
cur_list.remove(w.lower())
|
if w.lower() in cur_list:
|
||||||
removed = True
|
cur_list.remove(w.lower())
|
||||||
|
removed = True
|
||||||
|
|
||||||
|
elif isinstance(server_or_channel, discord.TextChannel):
|
||||||
|
async with self.settings.channel(server_or_channel).filter() as cur_list:
|
||||||
|
for w in words:
|
||||||
|
if w.lower() in cur_list:
|
||||||
|
cur_list.remove(w.lower())
|
||||||
|
removed = True
|
||||||
|
|
||||||
return removed
|
return removed
|
||||||
|
|
||||||
async def check_filter(self, message: discord.Message):
|
async def check_filter(self, message: discord.Message):
|
||||||
server = message.guild
|
server = message.guild
|
||||||
author = message.author
|
author = message.author
|
||||||
word_list = await self.settings.guild(server).filter()
|
word_list = set(
|
||||||
|
await self.settings.guild(server).filter()
|
||||||
|
+ await self.settings.channel(message.channel).filter()
|
||||||
|
)
|
||||||
filter_count = await self.settings.guild(server).filterban_count()
|
filter_count = await self.settings.guild(server).filterban_count()
|
||||||
filter_time = await self.settings.guild(server).filterban_time()
|
filter_time = await self.settings.guild(server).filterban_time()
|
||||||
user_count = await self.settings.member(author).filter_count()
|
user_count = await self.settings.member(author).filter_count()
|
||||||
@@ -211,7 +341,7 @@ class Filter:
|
|||||||
if w in message.content.lower():
|
if w in message.content.lower():
|
||||||
try:
|
try:
|
||||||
await message.delete()
|
await message.delete()
|
||||||
except:
|
except discord.HTTPException:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if filter_count > 0 and filter_time > 0:
|
if filter_count > 0 and filter_time > 0:
|
||||||
@@ -221,10 +351,10 @@ class Filter:
|
|||||||
user_count >= filter_count
|
user_count >= filter_count
|
||||||
and message.created_at.timestamp() < next_reset_time
|
and message.created_at.timestamp() < next_reset_time
|
||||||
):
|
):
|
||||||
reason = "Autoban (too many filtered messages.)"
|
reason = _("Autoban (too many filtered messages.)")
|
||||||
try:
|
try:
|
||||||
await server.ban(author, reason=reason)
|
await server.ban(author, reason=reason)
|
||||||
except:
|
except discord.HTTPException:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
await modlog.create_case(
|
await modlog.create_case(
|
||||||
@@ -245,74 +375,40 @@ class Filter:
|
|||||||
if not valid_user:
|
if not valid_user:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Bots and mods or superior are ignored from the filter
|
if await self.bot.is_automod_immune(message):
|
||||||
mod_or_superior = await is_mod_or_superior(self.bot, obj=author)
|
|
||||||
if mod_or_superior:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.check_filter(message)
|
await self.check_filter(message)
|
||||||
|
|
||||||
async def on_message_edit(self, _, message):
|
async def on_message_edit(self, _prior, message):
|
||||||
author = message.author
|
# message content has to change for non-bot's currently.
|
||||||
if message.guild is None or self.bot.user == author:
|
# if this changes, we should compare before passing it.
|
||||||
return
|
await self.on_message(message)
|
||||||
valid_user = isinstance(author, discord.Member) and not author.bot
|
|
||||||
if not valid_user:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Bots and mods or superior are ignored from the filter
|
|
||||||
mod_or_superior = await is_mod_or_superior(self.bot, obj=author)
|
|
||||||
if mod_or_superior:
|
|
||||||
return
|
|
||||||
|
|
||||||
await self.check_filter(message)
|
|
||||||
|
|
||||||
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
||||||
if not after.guild.me.guild_permissions.manage_nicknames:
|
if before.display_name != after.display_name:
|
||||||
return # No permissions to manage nicknames, so can't do anything
|
await self.maybe_filter_name(after)
|
||||||
word_list = await self.settings.guild(after.guild).filter()
|
|
||||||
filter_names = await self.settings.guild(after.guild).filter_names()
|
|
||||||
name_to_use = await self.settings.guild(after.guild).filter_default_name()
|
|
||||||
if not filter_names:
|
|
||||||
return
|
|
||||||
|
|
||||||
name_filtered = False
|
|
||||||
nick_filtered = False
|
|
||||||
|
|
||||||
for w in word_list:
|
|
||||||
if w in after.name:
|
|
||||||
name_filtered = True
|
|
||||||
if after.nick and w in after.nick: # since Member.nick can be None
|
|
||||||
nick_filtered = True
|
|
||||||
if name_filtered and nick_filtered: # Both true, so break from loop
|
|
||||||
break
|
|
||||||
|
|
||||||
if name_filtered and after.nick is None:
|
|
||||||
try:
|
|
||||||
await after.edit(nick=name_to_use, reason="Filtered name")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
elif nick_filtered:
|
|
||||||
try:
|
|
||||||
await after.edit(nick=None, reason="Filtered nickname")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_member_join(self, member: discord.Member):
|
async def on_member_join(self, member: discord.Member):
|
||||||
guild = member.guild
|
await self.maybe_filter_name(member)
|
||||||
if not guild.me.guild_permissions.manage_nicknames:
|
|
||||||
return
|
|
||||||
word_list = await self.settings.guild(guild).filter()
|
|
||||||
filter_names = await self.settings.guild(guild).filter_names()
|
|
||||||
name_to_use = await self.settings.guild(guild).filter_default_name()
|
|
||||||
|
|
||||||
if not filter_names:
|
async def maybe_filter_name(self, member: discord.Member):
|
||||||
|
if not member.guild.me.guild_permissions.manage_nicknames:
|
||||||
|
return # No permissions to manage nicknames, so can't do anything
|
||||||
|
if member.top_role >= member.guild.me.top_role:
|
||||||
|
return # Discord Hierarchy applies to nicks
|
||||||
|
if await self.bot.is_automod_immune(member):
|
||||||
|
return
|
||||||
|
if not await self.settings.guild(member.guild).filter_names():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
word_list = await self.settings.guild(member.guild).filter()
|
||||||
for w in word_list:
|
for w in word_list:
|
||||||
if w in member.name:
|
if w in member.display_name.lower():
|
||||||
|
name_to_use = await self.settings.guild(member.guild).filter_default_name()
|
||||||
|
reason = _("Filtered nickname") if member.nick else _("Filtered name")
|
||||||
try:
|
try:
|
||||||
await member.edit(nick=name_to_use, reason="Filtered name")
|
await member.edit(nick=name_to_use, reason=reason)
|
||||||
except:
|
except discord.HTTPException:
|
||||||
pass
|
pass
|
||||||
break
|
return
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ import datetime
|
|||||||
import time
|
import time
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from random import randint, choice
|
from random import randint, choice
|
||||||
from urllib.parse import quote_plus
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||||
from redbot.core.utils.chat_formatting import escape, italics, pagify
|
from redbot.core.utils.chat_formatting import escape, italics
|
||||||
|
|
||||||
_ = Translator("General", __file__)
|
_ = T_ = Translator("General", __file__)
|
||||||
|
|
||||||
|
|
||||||
class RPS(Enum):
|
class RPS(Enum):
|
||||||
@@ -29,70 +28,78 @@ class RPSParser:
|
|||||||
elif argument == "scissors":
|
elif argument == "scissors":
|
||||||
self.choice = RPS.scissors
|
self.choice = RPS.scissors
|
||||||
else:
|
else:
|
||||||
raise
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class General:
|
class General(commands.Cog):
|
||||||
"""General commands."""
|
"""General commands."""
|
||||||
|
|
||||||
|
global _
|
||||||
|
_ = lambda s: s
|
||||||
|
ball = [
|
||||||
|
_("As I see it, yes"),
|
||||||
|
_("It is certain"),
|
||||||
|
_("It is decidedly so"),
|
||||||
|
_("Most likely"),
|
||||||
|
_("Outlook good"),
|
||||||
|
_("Signs point to yes"),
|
||||||
|
_("Without a doubt"),
|
||||||
|
_("Yes"),
|
||||||
|
_("Yes – definitely"),
|
||||||
|
_("You may rely on it"),
|
||||||
|
_("Reply hazy, try again"),
|
||||||
|
_("Ask again later"),
|
||||||
|
_("Better not tell you now"),
|
||||||
|
_("Cannot predict now"),
|
||||||
|
_("Concentrate and ask again"),
|
||||||
|
_("Don't count on it"),
|
||||||
|
_("My reply is no"),
|
||||||
|
_("My sources say no"),
|
||||||
|
_("Outlook not so good"),
|
||||||
|
_("Very doubtful"),
|
||||||
|
]
|
||||||
|
_ = T_
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self.stopwatches = {}
|
self.stopwatches = {}
|
||||||
self.ball = [
|
|
||||||
_("As I see it, yes"),
|
|
||||||
_("It is certain"),
|
|
||||||
_("It is decidedly so"),
|
|
||||||
_("Most likely"),
|
|
||||||
_("Outlook good"),
|
|
||||||
_("Signs point to yes"),
|
|
||||||
_("Without a doubt"),
|
|
||||||
_("Yes"),
|
|
||||||
_("Yes – definitely"),
|
|
||||||
_("You may rely on it"),
|
|
||||||
_("Reply hazy, try again"),
|
|
||||||
_("Ask again later"),
|
|
||||||
_("Better not tell you now"),
|
|
||||||
_("Cannot predict now"),
|
|
||||||
_("Concentrate and ask again"),
|
|
||||||
_("Don't count on it"),
|
|
||||||
_("My reply is no"),
|
|
||||||
_("My sources say no"),
|
|
||||||
_("Outlook not so good"),
|
|
||||||
_("Very doubtful"),
|
|
||||||
]
|
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def choose(self, ctx, *choices):
|
async def choose(self, ctx, *choices):
|
||||||
"""Chooses between multiple choices.
|
"""Choose between multiple options.
|
||||||
|
|
||||||
To denote multiple choices, you should use double quotes.
|
To denote options which include whitespace, you should use
|
||||||
|
double quotes.
|
||||||
"""
|
"""
|
||||||
choices = [escape(c, mass_mentions=True) for c in choices]
|
choices = [escape(c, mass_mentions=True) for c in choices]
|
||||||
if len(choices) < 2:
|
if len(choices) < 2:
|
||||||
await ctx.send(_("Not enough choices to pick from."))
|
await ctx.send(_("Not enough options to pick from."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(choice(choices))
|
await ctx.send(choice(choices))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def roll(self, ctx, number: int = 100):
|
async def roll(self, ctx, number: int = 100):
|
||||||
"""Rolls random number (between 1 and user choice)
|
"""Roll a random number.
|
||||||
|
|
||||||
Defaults to 100.
|
The result will be between 1 and `<number>`.
|
||||||
|
|
||||||
|
`<number>` defaults to 100.
|
||||||
"""
|
"""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
if number > 1:
|
if number > 1:
|
||||||
n = randint(1, number)
|
n = randint(1, number)
|
||||||
await ctx.send(_("{} :game_die: {} :game_die:").format(author.mention, n))
|
await ctx.send("{author.mention} :game_die: {n} :game_die:".format(author=author, n=n))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("{} Maybe higher than 1? ;P").format(author.mention))
|
await ctx.send(_("{author.mention} Maybe higher than 1? ;P").format(author=author))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def flip(self, ctx, user: discord.Member = None):
|
async def flip(self, ctx, user: discord.Member = None):
|
||||||
"""Flips a coin... or a user.
|
"""Flip a coin... or a user.
|
||||||
|
|
||||||
Defaults to coin.
|
Defaults to a coin.
|
||||||
"""
|
"""
|
||||||
if user != None:
|
if user is not None:
|
||||||
msg = ""
|
msg = ""
|
||||||
if user.id == ctx.bot.user.id:
|
if user.id == ctx.bot.user.id:
|
||||||
user = ctx.author
|
user = ctx.author
|
||||||
@@ -111,7 +118,7 @@ class General:
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def rps(self, ctx, your_choice: RPSParser):
|
async def rps(self, ctx, your_choice: RPSParser):
|
||||||
"""Play rock paper scissors"""
|
"""Play Rock Paper Scissors."""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
player_choice = your_choice.choice
|
player_choice = your_choice.choice
|
||||||
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
|
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
|
||||||
@@ -130,39 +137,53 @@ class General:
|
|||||||
outcome = cond[(player_choice, red_choice)]
|
outcome = cond[(player_choice, red_choice)]
|
||||||
|
|
||||||
if outcome is True:
|
if outcome is True:
|
||||||
await ctx.send(_("{} You win {}!").format(red_choice.value, author.mention))
|
await ctx.send(
|
||||||
|
_("{choice} You win {author.mention}!").format(
|
||||||
|
choice=red_choice.value, author=author
|
||||||
|
)
|
||||||
|
)
|
||||||
elif outcome is False:
|
elif outcome is False:
|
||||||
await ctx.send(_("{} You lose {}!").format(red_choice.value, author.mention))
|
await ctx.send(
|
||||||
|
_("{choice} You lose {author.mention}!").format(
|
||||||
|
choice=red_choice.value, author=author
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("{} We're square {}!").format(red_choice.value, author.mention))
|
await ctx.send(
|
||||||
|
_("{choice} We're square {author.mention}!").format(
|
||||||
|
choice=red_choice.value, author=author
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@commands.command(name="8", aliases=["8ball"])
|
@commands.command(name="8", aliases=["8ball"])
|
||||||
async def _8ball(self, ctx, *, question: str):
|
async def _8ball(self, ctx, *, question: str):
|
||||||
"""Ask 8 ball a question
|
"""Ask 8 ball a question.
|
||||||
|
|
||||||
Question must end with a question mark.
|
Question must end with a question mark.
|
||||||
"""
|
"""
|
||||||
if question.endswith("?") and question != "?":
|
if question.endswith("?") and question != "?":
|
||||||
await ctx.send("`" + choice(self.ball) + "`")
|
await ctx.send("`" + T_(choice(self.ball)) + "`")
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("That doesn't look like a question."))
|
await ctx.send(_("That doesn't look like a question."))
|
||||||
|
|
||||||
@commands.command(aliases=["sw"])
|
@commands.command(aliases=["sw"])
|
||||||
async def stopwatch(self, ctx):
|
async def stopwatch(self, ctx):
|
||||||
"""Starts/stops stopwatch"""
|
"""Start or stop the stopwatch."""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
if not author.id in self.stopwatches:
|
if author.id not in self.stopwatches:
|
||||||
self.stopwatches[author.id] = int(time.perf_counter())
|
self.stopwatches[author.id] = int(time.perf_counter())
|
||||||
await ctx.send(author.mention + _(" Stopwatch started!"))
|
await ctx.send(author.mention + _(" Stopwatch started!"))
|
||||||
else:
|
else:
|
||||||
tmp = abs(self.stopwatches[author.id] - int(time.perf_counter()))
|
tmp = abs(self.stopwatches[author.id] - int(time.perf_counter()))
|
||||||
tmp = str(datetime.timedelta(seconds=tmp))
|
tmp = str(datetime.timedelta(seconds=tmp))
|
||||||
await ctx.send(author.mention + _(" Stopwatch stopped! Time: **") + tmp + "**")
|
await ctx.send(
|
||||||
|
author.mention + _(" Stopwatch stopped! Time: **{seconds}**").format(seconds=tmp)
|
||||||
|
)
|
||||||
self.stopwatches.pop(author.id, None)
|
self.stopwatches.pop(author.id, None)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def lmgtfy(self, ctx, *, search_terms: str):
|
async def lmgtfy(self, ctx, *, search_terms: str):
|
||||||
"""Creates a lmgtfy link"""
|
"""Create a lmgtfy link."""
|
||||||
search_terms = escape(
|
search_terms = escape(
|
||||||
search_terms.replace("+", "%2B").replace(" ", "+"), mass_mentions=True
|
search_terms.replace("+", "%2B").replace(" ", "+"), mass_mentions=True
|
||||||
)
|
)
|
||||||
@@ -171,9 +192,10 @@ class General:
|
|||||||
@commands.command(hidden=True)
|
@commands.command(hidden=True)
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def hug(self, ctx, user: discord.Member, intensity: int = 1):
|
async def hug(self, ctx, user: discord.Member, intensity: int = 1):
|
||||||
"""Because everyone likes hugs
|
"""Because everyone likes hugs!
|
||||||
|
|
||||||
Up to 10 intensity levels."""
|
Up to 10 intensity levels.
|
||||||
|
"""
|
||||||
name = italics(user.display_name)
|
name = italics(user.display_name)
|
||||||
if intensity <= 0:
|
if intensity <= 0:
|
||||||
msg = "(っ˘̩╭╮˘̩)っ" + name
|
msg = "(っ˘̩╭╮˘̩)っ" + name
|
||||||
@@ -185,27 +207,30 @@ class General:
|
|||||||
msg = "(つ≧▽≦)つ" + name
|
msg = "(つ≧▽≦)つ" + name
|
||||||
elif intensity >= 10:
|
elif intensity >= 10:
|
||||||
msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name)
|
msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name)
|
||||||
|
else:
|
||||||
|
# For the purposes of "msg might not be defined" linter errors
|
||||||
|
raise RuntimeError
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def serverinfo(self, ctx):
|
async def serverinfo(self, ctx):
|
||||||
"""Shows server's informations"""
|
"""Show server information."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
online = len([m.status for m in guild.members if m.status != discord.Status.offline])
|
online = len([m.status for m in guild.members if m.status != discord.Status.offline])
|
||||||
total_users = len(guild.members)
|
total_users = len(guild.members)
|
||||||
text_channels = len(guild.text_channels)
|
text_channels = len(guild.text_channels)
|
||||||
voice_channels = len(guild.voice_channels)
|
voice_channels = len(guild.voice_channels)
|
||||||
passed = (ctx.message.created_at - guild.created_at).days
|
passed = (ctx.message.created_at - guild.created_at).days
|
||||||
created_at = _("Since {}. That's over {} days ago!").format(
|
created_at = _("Since {date}. That's over {num} days ago!").format(
|
||||||
guild.created_at.strftime("%d %b %Y %H:%M"), passed
|
date=guild.created_at.strftime("%d %b %Y %H:%M"), num=passed
|
||||||
)
|
)
|
||||||
data = discord.Embed(description=created_at, colour=(await ctx.embed_colour()))
|
data = discord.Embed(description=created_at, colour=(await ctx.embed_colour()))
|
||||||
data.add_field(name=_("Region"), value=str(guild.region))
|
data.add_field(name=_("Region"), value=str(guild.region))
|
||||||
data.add_field(name=_("Users"), value="{}/{}".format(online, total_users))
|
data.add_field(name=_("Users"), value=f"{online}/{total_users}")
|
||||||
data.add_field(name=_("Text Channels"), value=text_channels)
|
data.add_field(name=_("Text Channels"), value=str(text_channels))
|
||||||
data.add_field(name=_("Voice Channels"), value=voice_channels)
|
data.add_field(name=_("Voice Channels"), value=str(voice_channels))
|
||||||
data.add_field(name=_("Roles"), value=len(guild.roles))
|
data.add_field(name=_("Roles"), value=str(len(guild.roles)))
|
||||||
data.add_field(name=_("Owner"), value=str(guild.owner))
|
data.add_field(name=_("Owner"), value=str(guild.owner))
|
||||||
data.set_footer(text=_("Server ID: ") + str(guild.id))
|
data.set_footer(text=_("Server ID: ") + str(guild.id))
|
||||||
|
|
||||||
@@ -217,12 +242,15 @@ class General:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await ctx.send(embed=data)
|
await ctx.send(embed=data)
|
||||||
except discord.HTTPException:
|
except discord.Forbidden:
|
||||||
await ctx.send(_("I need the `Embed links` permission to send this."))
|
await ctx.send(_("I need the `Embed links` permission to send this."))
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def urban(self, ctx, *, word):
|
async def urban(self, ctx, *, word):
|
||||||
"""Searches urban dictionary entries using the unofficial api"""
|
"""Search the Urban Dictionary.
|
||||||
|
|
||||||
|
This uses the unofficial Urban Dictionary API.
|
||||||
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
url = "https://api.urbandictionary.com/v0/define?term=" + str(word).lower()
|
url = "https://api.urbandictionary.com/v0/define?term=" + str(word).lower()
|
||||||
@@ -233,10 +261,11 @@ class General:
|
|||||||
async with session.get(url, headers=headers) as response:
|
async with session.get(url, headers=headers) as response:
|
||||||
data = await response.json()
|
data = await response.json()
|
||||||
|
|
||||||
except:
|
except aiohttp.ClientError:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("No Urban dictionary entries were found or there was an error in the process")
|
_("No Urban dictionary entries were found, or there was an error in the process")
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if data.get("error") != 404:
|
if data.get("error") != 404:
|
||||||
|
|
||||||
@@ -245,20 +274,20 @@ class General:
|
|||||||
embeds = []
|
embeds = []
|
||||||
for ud in data["list"]:
|
for ud in data["list"]:
|
||||||
embed = discord.Embed()
|
embed = discord.Embed()
|
||||||
embed.title = _("{} by {}").format(ud["word"].capitalize(), ud["author"])
|
embed.title = _("{word} by {author}").format(
|
||||||
|
word=ud["word"].capitalize(), author=ud["author"]
|
||||||
|
)
|
||||||
embed.url = ud["permalink"]
|
embed.url = ud["permalink"]
|
||||||
|
|
||||||
description = "{} \n \n **Example : ** {}".format(
|
description = _("{definition}\n\n**Example:** {example}").format(**ud)
|
||||||
ud["definition"], ud.get("example", "N/A")
|
|
||||||
)
|
|
||||||
if len(description) > 2048:
|
if len(description) > 2048:
|
||||||
description = "{}...".format(description[:2045])
|
description = "{}...".format(description[:2045])
|
||||||
embed.description = description
|
embed.description = description
|
||||||
|
|
||||||
embed.set_footer(
|
embed.set_footer(
|
||||||
text=_("{} Down / {} Up , Powered by urban dictionary").format(
|
text=_(
|
||||||
ud["thumbs_down"], ud["thumbs_up"]
|
"{thumbs_down} Down / {thumbs_up} Up, Powered by Urban Dictionary."
|
||||||
)
|
).format(**ud)
|
||||||
)
|
)
|
||||||
embeds.append(embed)
|
embeds.append(embed)
|
||||||
|
|
||||||
@@ -274,24 +303,15 @@ class General:
|
|||||||
else:
|
else:
|
||||||
messages = []
|
messages = []
|
||||||
for ud in data["list"]:
|
for ud in data["list"]:
|
||||||
description = _("{} \n \n **Example : ** {}").format(
|
ud.set_default("example", "N/A")
|
||||||
ud["definition"], ud.get("example", "N/A")
|
description = _("{definition}\n\n**Example:** {example}").format(**ud)
|
||||||
)
|
|
||||||
if len(description) > 2048:
|
if len(description) > 2048:
|
||||||
description = "{}...".format(description[:2045])
|
description = "{}...".format(description[:2045])
|
||||||
description = description
|
|
||||||
|
|
||||||
message = _(
|
message = _(
|
||||||
"<{}> \n {} by {} \n \n {} \n \n {} Down / {} Up, Powered by urban "
|
"<{permalink}>\n {word} by {author}\n\n{description}\n\n"
|
||||||
"dictionary"
|
"{thumbs_down} Down / {thumbs_up} Up, Powered by urban dictionary"
|
||||||
).format(
|
).format(word=ud.pop("word").capitalize(), description=description, **ud)
|
||||||
ud["permalink"],
|
|
||||||
ud["word"].capitalize(),
|
|
||||||
ud["author"],
|
|
||||||
description,
|
|
||||||
ud["thumbs_down"],
|
|
||||||
ud["thumbs_up"],
|
|
||||||
)
|
|
||||||
messages.append(message)
|
messages.append(message)
|
||||||
|
|
||||||
if messages is not None and len(messages) > 0:
|
if messages is not None and len(messages) > 0:
|
||||||
@@ -305,6 +325,6 @@ class General:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("No Urban dictionary entries were found or there was an error in the process")
|
_("No Urban dictionary entries were found, or there was an error in the process.")
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ GIPHY_API_KEY = "dc6zaTOxFJmzC"
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Image:
|
class Image(commands.Cog):
|
||||||
"""Image related commands."""
|
"""Image related commands."""
|
||||||
|
|
||||||
default_global = {"imgur_client_id": None}
|
default_global = {"imgur_client_id": None}
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.settings = Config.get_conf(self, identifier=2652104208, force_registration=True)
|
self.settings = Config.get_conf(self, identifier=2652104208, force_registration=True)
|
||||||
self.settings.register_global(**self.default_global)
|
self.settings.register_global(**self.default_global)
|
||||||
@@ -28,23 +29,26 @@ class Image:
|
|||||||
|
|
||||||
@commands.group(name="imgur")
|
@commands.group(name="imgur")
|
||||||
async def _imgur(self, ctx):
|
async def _imgur(self, ctx):
|
||||||
"""Retrieves pictures from imgur
|
"""Retrieve pictures from Imgur.
|
||||||
|
|
||||||
Make sure to set the client ID using
|
Make sure to set the Client ID using `[p]imgurcreds`.
|
||||||
[p]imgurcreds"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@_imgur.command(name="search")
|
@_imgur.command(name="search")
|
||||||
async def imgur_search(self, ctx, *, term: str):
|
async def imgur_search(self, ctx, *, term: str):
|
||||||
"""Searches Imgur for the specified term and returns up to 3 results"""
|
"""Search Imgur for the specified term.
|
||||||
|
|
||||||
|
Returns up to 3 results.
|
||||||
|
"""
|
||||||
url = self.imgur_base_url + "gallery/search/time/all/0"
|
url = self.imgur_base_url + "gallery/search/time/all/0"
|
||||||
params = {"q": term}
|
params = {"q": term}
|
||||||
imgur_client_id = await self.settings.imgur_client_id()
|
imgur_client_id = await self.settings.imgur_client_id()
|
||||||
if not imgur_client_id:
|
if not imgur_client_id:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("A client ID has not been set! Please set one with {}.").format(
|
_(
|
||||||
"`{}imgurcreds`".format(ctx.prefix)
|
"A Client ID has not been set! Please set one with `{prefix}imgurcreds`."
|
||||||
)
|
).format(prefix=ctx.prefix)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
headers = {"Authorization": "Client-ID {}".format(imgur_client_id)}
|
headers = {"Authorization": "Client-ID {}".format(imgur_client_id)}
|
||||||
@@ -63,37 +67,41 @@ class Image:
|
|||||||
msg += "\n"
|
msg += "\n"
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Something went wrong. Error code is {}.").format(data["status"]))
|
await ctx.send(
|
||||||
|
_("Something went wrong. Error code is {code}.").format(code=data["status"])
|
||||||
|
)
|
||||||
|
|
||||||
@_imgur.command(name="subreddit")
|
@_imgur.command(name="subreddit")
|
||||||
async def imgur_subreddit(
|
async def imgur_subreddit(
|
||||||
self, ctx, subreddit: str, sort_type: str = "top", window: str = "day"
|
self, ctx, subreddit: str, sort_type: str = "top", window: str = "day"
|
||||||
):
|
):
|
||||||
"""Gets images from the specified subreddit section
|
"""Get images from a subreddit.
|
||||||
|
|
||||||
Sort types: new, top
|
You can customize the search with the following options:
|
||||||
Time windows: day, week, month, year, all"""
|
- `<sort_type>`: new, top
|
||||||
|
- `<window>`: day, week, month, year, all
|
||||||
|
"""
|
||||||
sort_type = sort_type.lower()
|
sort_type = sort_type.lower()
|
||||||
window = window.lower()
|
window = window.lower()
|
||||||
|
|
||||||
if sort_type not in ("new", "top"):
|
|
||||||
await ctx.send(_("Only 'new' and 'top' are a valid sort type."))
|
|
||||||
return
|
|
||||||
elif window not in ("day", "week", "month", "year", "all"):
|
|
||||||
await ctx.send_help()
|
|
||||||
return
|
|
||||||
|
|
||||||
if sort_type == "new":
|
if sort_type == "new":
|
||||||
sort = "time"
|
sort = "time"
|
||||||
elif sort_type == "top":
|
elif sort_type == "top":
|
||||||
sort = "top"
|
sort = "top"
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Only 'new' and 'top' are a valid sort type."))
|
||||||
|
return
|
||||||
|
|
||||||
|
if window not in ("day", "week", "month", "year", "all"):
|
||||||
|
await ctx.send_help()
|
||||||
|
return
|
||||||
|
|
||||||
imgur_client_id = await self.settings.imgur_client_id()
|
imgur_client_id = await self.settings.imgur_client_id()
|
||||||
if not imgur_client_id:
|
if not imgur_client_id:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("A client ID has not been set! Please set one with {}.").format(
|
_(
|
||||||
"`{}imgurcreds`".format(ctx.prefix)
|
"A Client ID has not been set! Please set one with `{prefix}imgurcreds`."
|
||||||
)
|
).format(prefix=ctx.prefix)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -116,28 +124,33 @@ class Image:
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("No results found."))
|
await ctx.send(_("No results found."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Something went wrong. Error code is {}.").format(data["status"]))
|
await ctx.send(
|
||||||
|
_("Something went wrong. Error code is {code}.").format(code=data["status"])
|
||||||
|
)
|
||||||
|
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def imgurcreds(self, ctx, imgur_client_id: str):
|
async def imgurcreds(self, ctx, imgur_client_id: str):
|
||||||
"""Sets the imgur client id
|
"""Set the Imgur Client ID.
|
||||||
|
|
||||||
You will need an account on Imgur to get this
|
To get an Imgur Client ID:
|
||||||
|
1. Login to an Imgur account.
|
||||||
You can get these by visiting https://api.imgur.com/oauth2/addclient
|
2. Visit [this](https://api.imgur.com/oauth2/addclient) page
|
||||||
and filling out the form. Enter a name for the application, select
|
3. Enter a name for your application
|
||||||
'Anonymous usage without user authorization' for the auth type,
|
4. Select *Anonymous usage without user authorization* for the auth type
|
||||||
set the authorization callback url to 'https://localhost'
|
5. Set the authorization callback URL to `https://localhost`
|
||||||
leave the app website blank, enter a valid email address, and
|
6. Leave the app website blank
|
||||||
enter a description. Check the box for the captcha, then click Next.
|
7. Enter a valid email address and a description
|
||||||
Your client ID will be on the page that loads."""
|
8. Check the captcha box and click next
|
||||||
|
9. Your Client ID will be on the next page.
|
||||||
|
"""
|
||||||
await self.settings.imgur_client_id.set(imgur_client_id)
|
await self.settings.imgur_client_id.set(imgur_client_id)
|
||||||
await ctx.send(_("Set the imgur client id!"))
|
await ctx.send(_("The Imgur Client ID has been set!"))
|
||||||
|
|
||||||
@commands.command(pass_context=True, no_pm=True)
|
@commands.guild_only()
|
||||||
|
@commands.command()
|
||||||
async def gif(self, ctx, *keywords):
|
async def gif(self, ctx, *keywords):
|
||||||
"""Retrieves first search result from giphy"""
|
"""Retrieve the first search result from Giphy."""
|
||||||
if keywords:
|
if keywords:
|
||||||
keywords = "+".join(keywords)
|
keywords = "+".join(keywords)
|
||||||
else:
|
else:
|
||||||
@@ -156,11 +169,12 @@ class Image:
|
|||||||
else:
|
else:
|
||||||
await ctx.send(_("No results found."))
|
await ctx.send(_("No results found."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Error contacting the API."))
|
await ctx.send(_("Error contacting the Giphy API."))
|
||||||
|
|
||||||
@commands.command(pass_context=True, no_pm=True)
|
@commands.guild_only()
|
||||||
|
@commands.command()
|
||||||
async def gifr(self, ctx, *keywords):
|
async def gifr(self, ctx, *keywords):
|
||||||
"""Retrieves a random gif from a giphy search"""
|
"""Retrieve a random GIF from a Giphy search."""
|
||||||
if keywords:
|
if keywords:
|
||||||
keywords = "+".join(keywords)
|
keywords = "+".join(keywords)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
import discord
|
|
||||||
|
|
||||||
|
|
||||||
def mod_or_voice_permissions(**perms):
|
def mod_or_voice_permissions(**perms):
|
||||||
@@ -10,8 +9,8 @@ def mod_or_voice_permissions(**perms):
|
|||||||
# Author is bot owner or guild owner
|
# Author is bot owner or guild owner
|
||||||
return True
|
return True
|
||||||
|
|
||||||
admin_role = discord.utils.get(guild.roles, id=await ctx.bot.db.guild(guild).admin_role())
|
admin_role = guild.get_role(await ctx.bot.db.guild(guild).admin_role())
|
||||||
mod_role = discord.utils.get(guild.roles, id=await ctx.bot.db.guild(guild).mod_role())
|
mod_role = guild.get_role(await ctx.bot.db.guild(guild).mod_role())
|
||||||
|
|
||||||
if admin_role in author.roles or mod_role in author.roles:
|
if admin_role in author.roles or mod_role in author.roles:
|
||||||
return True
|
return True
|
||||||
@@ -26,7 +25,7 @@ def mod_or_voice_permissions(**perms):
|
|||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.permissions_check(pred)
|
||||||
|
|
||||||
|
|
||||||
def admin_or_voice_permissions(**perms):
|
def admin_or_voice_permissions(**perms):
|
||||||
@@ -35,7 +34,7 @@ def admin_or_voice_permissions(**perms):
|
|||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if await ctx.bot.is_owner(author) or guild.owner == author:
|
if await ctx.bot.is_owner(author) or guild.owner == author:
|
||||||
return True
|
return True
|
||||||
admin_role = discord.utils.get(guild.roles, id=await ctx.bot.db.guild(guild).admin_role())
|
admin_role = guild.get_role(await ctx.bot.db.guild(guild).admin_role())
|
||||||
if admin_role in author.roles:
|
if admin_role in author.roles:
|
||||||
return True
|
return True
|
||||||
for vc in guild.voice_channels:
|
for vc in guild.voice_channels:
|
||||||
@@ -48,7 +47,7 @@ def admin_or_voice_permissions(**perms):
|
|||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return commands.check(pred)
|
return commands.permissions_check(pred)
|
||||||
|
|
||||||
|
|
||||||
def bot_has_voice_permissions(**perms):
|
def bot_has_voice_permissions(**perms):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from redbot.core import checks, modlog, commands
|
from redbot.core import checks, modlog, commands
|
||||||
@@ -9,32 +11,38 @@ _ = Translator("ModLog", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class ModLog:
|
class ModLog(commands.Cog):
|
||||||
"""Log for mod actions"""
|
"""Manage log channels for moderation actions."""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def modlogset(self, ctx: commands.Context):
|
async def modlogset(self, ctx: commands.Context):
|
||||||
"""Settings for the mod log"""
|
"""Manage modlog settings."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@modlogset.command()
|
@modlogset.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def modlog(self, ctx: commands.Context, channel: discord.TextChannel = None):
|
async def modlog(self, ctx: commands.Context, channel: discord.TextChannel = None):
|
||||||
"""Sets a channel as mod log
|
"""Set a channel as the modlog.
|
||||||
|
|
||||||
Leaving the channel parameter empty will deactivate it"""
|
Omit `<channel>` to disable the modlog.
|
||||||
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
if channel:
|
if channel:
|
||||||
if channel.permissions_for(guild.me).send_messages:
|
if channel.permissions_for(guild.me).send_messages:
|
||||||
await modlog.set_modlog_channel(guild, channel)
|
await modlog.set_modlog_channel(guild, channel)
|
||||||
await ctx.send(_("Mod events will be sent to {}").format(channel.mention))
|
await ctx.send(
|
||||||
|
_("Mod events will be sent to {channel}").format(channel=channel.mention)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("I do not have permissions to send messages in {}!").format(channel.mention)
|
_("I do not have permissions to send messages in {channel}!").format(
|
||||||
|
channel=channel.mention
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
@@ -48,39 +56,36 @@ class ModLog:
|
|||||||
@modlogset.command(name="cases")
|
@modlogset.command(name="cases")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def set_cases(self, ctx: commands.Context, action: str = None):
|
async def set_cases(self, ctx: commands.Context, action: str = None):
|
||||||
"""Enables or disables case creation for each type of mod action"""
|
"""Enable or disable case creation for a mod action."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
|
||||||
if action is None: # No args given
|
if action is None: # No args given
|
||||||
casetypes = await modlog.get_all_casetypes(guild)
|
casetypes = await modlog.get_all_casetypes(guild)
|
||||||
await ctx.send_help()
|
await ctx.send_help()
|
||||||
title = _("Current settings:")
|
lines = []
|
||||||
msg = ""
|
|
||||||
for ct in casetypes:
|
for ct in casetypes:
|
||||||
enabled = await ct.is_enabled()
|
enabled = "enabled" if await ct.is_enabled() else "disabled"
|
||||||
value = "enabled" if enabled else "disabled"
|
lines.append(f"{ct.name} : {enabled}")
|
||||||
msg += "%s : %s\n" % (ct.name, value)
|
|
||||||
|
|
||||||
msg = title + "\n" + box(msg)
|
await ctx.send(_("Current settings:\n") + box("\n".join(lines)))
|
||||||
await ctx.send(msg)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
casetype = await modlog.get_casetype(action, guild)
|
casetype = await modlog.get_casetype(action, guild)
|
||||||
if not casetype:
|
if not casetype:
|
||||||
await ctx.send(_("That action is not registered"))
|
await ctx.send(_("That action is not registered"))
|
||||||
else:
|
else:
|
||||||
|
|
||||||
enabled = await casetype.is_enabled()
|
enabled = await casetype.is_enabled()
|
||||||
await casetype.set_enabled(True if not enabled else False)
|
await casetype.set_enabled(not enabled)
|
||||||
|
await ctx.send(
|
||||||
msg = _("Case creation for {} actions is now {}.").format(
|
_("Case creation for {action_name} actions is now {enabled}.").format(
|
||||||
action, "enabled" if not enabled else "disabled"
|
action_name=action, enabled="enabled" if not enabled else "disabled"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
await ctx.send(msg)
|
|
||||||
|
|
||||||
@modlogset.command()
|
@modlogset.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def resetcases(self, ctx: commands.Context):
|
async def resetcases(self, ctx: commands.Context):
|
||||||
"""Resets modlog's cases"""
|
"""Reset all modlog cases in this server."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
await modlog.reset_cases(guild)
|
await modlog.reset_cases(guild)
|
||||||
await ctx.send(_("Cases have been reset."))
|
await ctx.send(_("Cases have been reset."))
|
||||||
@@ -88,7 +93,7 @@ class ModLog:
|
|||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def case(self, ctx: commands.Context, number: int):
|
async def case(self, ctx: commands.Context, number: int):
|
||||||
"""Shows the specified case"""
|
"""Show the specified case."""
|
||||||
try:
|
try:
|
||||||
case = await modlog.get_case(number, ctx.guild, self.bot)
|
case = await modlog.get_case(number, ctx.guild, self.bot)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
@@ -100,24 +105,21 @@ class ModLog:
|
|||||||
else:
|
else:
|
||||||
await ctx.send(await case.message_content(embed=False))
|
await ctx.send(await case.message_content(embed=False))
|
||||||
|
|
||||||
@commands.command(usage="[case] <reason>")
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def reason(self, ctx: commands.Context, *, reason: str):
|
async def reason(self, ctx: commands.Context, case: Optional[int], *, reason: str):
|
||||||
"""Lets you specify a reason for mod-log's cases
|
"""Specify a reason for a modlog case.
|
||||||
|
|
||||||
Please note that you can only edit cases you are
|
Please note that you can only edit cases you are
|
||||||
the owner of unless you are a mod/admin or the server owner.
|
the owner of unless you are a mod, admin or server owner.
|
||||||
|
|
||||||
If no number is specified, the latest case will be used."""
|
If no case number is specified, the latest case will be used.
|
||||||
|
"""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
potential_case = reason.split()[0]
|
if case is None:
|
||||||
if potential_case.isdigit():
|
# get the latest case
|
||||||
case = int(potential_case)
|
case = int(await modlog.get_next_case_number(guild)) - 1
|
||||||
reason = reason.replace(potential_case, "")
|
|
||||||
else:
|
|
||||||
case = str(int(await modlog.get_next_case_number(guild)) - 1)
|
|
||||||
# latest case
|
|
||||||
try:
|
try:
|
||||||
case_before = await modlog.get_case(case, guild, self.bot)
|
case_before = await modlog.get_case(case, guild, self.bot)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
from .permissions import Permissions
|
from .permissions import Permissions
|
||||||
|
|
||||||
|
|
||||||
def setup(bot):
|
async def setup(bot):
|
||||||
bot.add_cog(Permissions(bot))
|
cog = Permissions(bot)
|
||||||
|
await cog.initialize()
|
||||||
|
# It's important that these listeners are added prior to load, so
|
||||||
|
# the permissions commands themselves have rules added.
|
||||||
|
# Automatic listeners being added in add_cog happen in arbitrary
|
||||||
|
# order, so we want to circumvent that.
|
||||||
|
bot.add_listener(cog.cog_added, "on_cog_add")
|
||||||
|
bot.add_listener(cog.command_added, "on_command_add")
|
||||||
|
bot.add_cog(cog)
|
||||||
|
|||||||
@@ -1,44 +1,55 @@
|
|||||||
|
from typing import NamedTuple, Union, Optional, cast, Type
|
||||||
|
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
from typing import Tuple
|
from redbot.core.i18n import Translator
|
||||||
|
|
||||||
|
_ = Translator("PermissionsConverters", __file__)
|
||||||
|
|
||||||
|
|
||||||
class CogOrCommand(commands.Converter):
|
class CogOrCommand(NamedTuple):
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> Tuple[str]:
|
type: str
|
||||||
ret = ctx.bot.get_cog(arg)
|
name: str
|
||||||
if ret:
|
obj: Union[commands.Command, commands.Cog]
|
||||||
return "cogs", ret.__class__.__name__
|
|
||||||
ret = ctx.bot.get_command(arg)
|
# noinspection PyArgumentList
|
||||||
if ret:
|
@classmethod
|
||||||
return "commands", ret.qualified_name
|
async def convert(cls, ctx: commands.Context, arg: str) -> "CogOrCommand":
|
||||||
|
cog = ctx.bot.get_cog(arg)
|
||||||
|
if cog:
|
||||||
|
return cls(type="COG", name=cog.__class__.__name__, obj=cog)
|
||||||
|
cmd = ctx.bot.get_command(arg)
|
||||||
|
if cmd:
|
||||||
|
return cls(type="COMMAND", name=cmd.qualified_name, obj=cmd)
|
||||||
|
|
||||||
raise commands.BadArgument(
|
raise commands.BadArgument(
|
||||||
'Cog or command "{arg}" not found. Please note that this is case sensitive.'
|
_(
|
||||||
"".format(arg=arg)
|
'Cog or command "{name}" not found. Please note that this is case sensitive.'
|
||||||
|
).format(name=arg)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RuleType(commands.Converter):
|
def RuleType(arg: str) -> bool:
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||||
if arg.lower() in ("allow", "whitelist", "allowed"):
|
return True
|
||||||
return "allow"
|
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||||
if arg.lower() in ("deny", "blacklist", "denied"):
|
return False
|
||||||
return "deny"
|
|
||||||
|
|
||||||
raise commands.BadArgument(
|
raise commands.BadArgument(
|
||||||
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny"'.format(arg=arg)
|
_('"{arg}" is not a valid rule. Valid rules are "allow" or "deny"').format(arg=arg)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ClearableRuleType(commands.Converter):
|
def ClearableRuleType(arg: str) -> Optional[bool]:
|
||||||
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||||
if arg.lower() in ("allow", "whitelist", "allowed"):
|
return True
|
||||||
return "allow"
|
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||||
if arg.lower() in ("deny", "blacklist", "denied"):
|
return False
|
||||||
return "deny"
|
if arg.lower() in ("clear", "reset"):
|
||||||
if arg.lower() in ("clear", "reset"):
|
return None
|
||||||
return "clear"
|
|
||||||
|
|
||||||
raise commands.BadArgument(
|
raise commands.BadArgument(
|
||||||
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny", or "clear" to remove the rule'
|
_(
|
||||||
"".format(arg=arg)
|
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny", or "clear" to '
|
||||||
)
|
"remove the rule"
|
||||||
|
).format(arg=arg)
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
from redbot.core import commands
|
|
||||||
from redbot.core.config import Config
|
|
||||||
from .resolvers import entries_from_ctx, resolve_lists
|
|
||||||
|
|
||||||
# This has optimizations in it that may not hold True if other parts of the permission
|
|
||||||
# model are changed from the state they are in currently.
|
|
||||||
# (commit hash ~ 3bcf375204c22271ad3ed1fc059b598b751aa03f)
|
|
||||||
#
|
|
||||||
# This is primarily to help with the performance of the help formatter
|
|
||||||
|
|
||||||
# This is less efficient if only checking one command,
|
|
||||||
# but is much faster for checking all of them.
|
|
||||||
|
|
||||||
|
|
||||||
async def mass_resolve(*, ctx: commands.Context, config: Config):
|
|
||||||
"""
|
|
||||||
Get's all the permission cog interactions for all loaded commands
|
|
||||||
in the given context.
|
|
||||||
"""
|
|
||||||
|
|
||||||
owner_settings = await config.owner_models()
|
|
||||||
guild_owner_settings = await config.guild(ctx.guild).owner_models() if ctx.guild else None
|
|
||||||
|
|
||||||
ret = {"allowed": [], "denied": [], "default": []}
|
|
||||||
|
|
||||||
for cogname, cog in ctx.bot.cogs.items():
|
|
||||||
|
|
||||||
cog_setting = resolve_cog_or_command(
|
|
||||||
objname=cogname, models=owner_settings, ctx=ctx, typ="cogs"
|
|
||||||
)
|
|
||||||
if cog_setting is None and guild_owner_settings:
|
|
||||||
cog_setting = resolve_cog_or_command(
|
|
||||||
objname=cogname, models=guild_owner_settings, ctx=ctx, typ="cogs"
|
|
||||||
)
|
|
||||||
|
|
||||||
for command in [c for c in ctx.bot.all_commands.values() if c.instance is cog]:
|
|
||||||
resolution = recursively_resolve(
|
|
||||||
com_or_group=command,
|
|
||||||
o_models=owner_settings,
|
|
||||||
g_models=guild_owner_settings,
|
|
||||||
ctx=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
for com, resolved in resolution:
|
|
||||||
if resolved is None:
|
|
||||||
resolved = cog_setting
|
|
||||||
if resolved is True:
|
|
||||||
ret["allowed"].append(com)
|
|
||||||
elif resolved is False:
|
|
||||||
ret["denied"].append(com)
|
|
||||||
else:
|
|
||||||
ret["default"].append(com)
|
|
||||||
|
|
||||||
ret = {k: set(v) for k, v in ret.items()}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def recursively_resolve(*, com_or_group, o_models, g_models, ctx, override=False):
|
|
||||||
ret = []
|
|
||||||
if override:
|
|
||||||
current = False
|
|
||||||
else:
|
|
||||||
current = resolve_cog_or_command(
|
|
||||||
typ="commands", objname=com_or_group.qualified_name, ctx=ctx, models=o_models
|
|
||||||
)
|
|
||||||
if current is None and g_models:
|
|
||||||
current = resolve_cog_or_command(
|
|
||||||
typ="commands", objname=com_or_group.qualified_name, ctx=ctx, models=o_models
|
|
||||||
)
|
|
||||||
ret.append((com_or_group, current))
|
|
||||||
if isinstance(com_or_group, commands.Group):
|
|
||||||
for com in com_or_group.commands:
|
|
||||||
ret.extend(
|
|
||||||
recursively_resolve(
|
|
||||||
com_or_group=com,
|
|
||||||
o_models=o_models,
|
|
||||||
g_models=g_models,
|
|
||||||
ctx=ctx,
|
|
||||||
override=(current is False),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_cog_or_command(*, typ, ctx, objname, models: dict) -> bool:
|
|
||||||
"""
|
|
||||||
Resolves models in order.
|
|
||||||
"""
|
|
||||||
|
|
||||||
resolved = None
|
|
||||||
|
|
||||||
if objname in models.get(typ, {}):
|
|
||||||
blacklist = models[typ][objname].get("deny", [])
|
|
||||||
whitelist = models[typ][objname].get("allow", [])
|
|
||||||
resolved = resolve_lists(ctx=ctx, whitelist=whitelist, blacklist=blacklist)
|
|
||||||
if resolved is not None:
|
|
||||||
return resolved
|
|
||||||
resolved = models[typ][objname].get("default", None)
|
|
||||||
if resolved is not None:
|
|
||||||
return resolved
|
|
||||||
return None
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,81 +0,0 @@
|
|||||||
import types
|
|
||||||
import contextlib
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
from redbot.core import commands
|
|
||||||
|
|
||||||
log = logging.getLogger("redbot.cogs.permissions.resolvers")
|
|
||||||
|
|
||||||
|
|
||||||
def entries_from_ctx(ctx: commands.Context) -> tuple:
|
|
||||||
voice_channel = None
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
voice_channel = ctx.author.voice.voice_channel
|
|
||||||
entries = [x.id for x in (ctx.author, voice_channel, ctx.channel) if x]
|
|
||||||
roles = sorted(ctx.author.roles, reverse=True) if ctx.guild else []
|
|
||||||
entries.extend([x.id for x in roles])
|
|
||||||
# entries now contains the following (in order) (if applicable)
|
|
||||||
# author.id
|
|
||||||
# author.voice.voice_channel.id
|
|
||||||
# channel.id
|
|
||||||
# role.id for each role (highest to lowest)
|
|
||||||
# (implicitly) guild.id because
|
|
||||||
# the @everyone role shares an id with the guild
|
|
||||||
return tuple(entries)
|
|
||||||
|
|
||||||
|
|
||||||
async def val_if_check_is_valid(*, ctx: commands.Context, check: object, level: str) -> bool:
|
|
||||||
"""
|
|
||||||
Returns the value from a check if it is valid
|
|
||||||
"""
|
|
||||||
|
|
||||||
val = None
|
|
||||||
# let's not spam the console with improperly made 3rd party checks
|
|
||||||
try:
|
|
||||||
if asyncio.iscoroutinefunction(check):
|
|
||||||
val = await check(ctx, level=level)
|
|
||||||
else:
|
|
||||||
val = check(ctx, level=level)
|
|
||||||
except Exception as e:
|
|
||||||
# but still provide a way to view it (run with debug flag)
|
|
||||||
log.debug(str(e))
|
|
||||||
|
|
||||||
return val
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_models(*, ctx: commands.Context, models: dict) -> bool:
|
|
||||||
"""
|
|
||||||
Resolves models in order.
|
|
||||||
"""
|
|
||||||
|
|
||||||
cmd_name = ctx.command.qualified_name
|
|
||||||
cog_name = ctx.cog.__class__.__name__
|
|
||||||
|
|
||||||
resolved = None
|
|
||||||
|
|
||||||
to_iter = (("commands", cmd_name), ("cogs", cog_name))
|
|
||||||
|
|
||||||
for model_name, ctx_attr in to_iter:
|
|
||||||
if ctx_attr in models.get(model_name, {}):
|
|
||||||
blacklist = models[model_name][ctx_attr].get("deny", [])
|
|
||||||
whitelist = models[model_name][ctx_attr].get("allow", [])
|
|
||||||
resolved = resolve_lists(ctx=ctx, whitelist=whitelist, blacklist=blacklist)
|
|
||||||
if resolved is not None:
|
|
||||||
return resolved
|
|
||||||
resolved = models[model_name][ctx_attr].get("default", None)
|
|
||||||
if resolved is not None:
|
|
||||||
return resolved
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_lists(*, ctx: commands.Context, whitelist: list, blacklist: list) -> bool:
|
|
||||||
"""
|
|
||||||
resolves specific lists
|
|
||||||
"""
|
|
||||||
for entry in entries_from_ctx(ctx):
|
|
||||||
if entry in whitelist:
|
|
||||||
return True
|
|
||||||
if entry in blacklist:
|
|
||||||
return False
|
|
||||||
return None
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
cogs:
|
|
||||||
Admin:
|
|
||||||
allow:
|
|
||||||
- 78631113035100160
|
|
||||||
deny:
|
|
||||||
- 96733288462286848
|
|
||||||
Audio:
|
|
||||||
allow:
|
|
||||||
- 133049272517001216
|
|
||||||
default: deny
|
|
||||||
commands:
|
|
||||||
cleanup bot:
|
|
||||||
allow:
|
|
||||||
- 78631113035100160
|
|
||||||
default: deny
|
|
||||||
ping:
|
|
||||||
deny:
|
|
||||||
- 96733288462286848
|
|
||||||
default: allow
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import io
|
|
||||||
import yaml
|
|
||||||
import pathlib
|
|
||||||
import discord
|
|
||||||
|
|
||||||
|
|
||||||
def yaml_template() -> dict:
|
|
||||||
template_fp = pathlib.Path(__file__).parent / "template.yaml"
|
|
||||||
|
|
||||||
with template_fp.open() as f:
|
|
||||||
return yaml.safe_load(f)
|
|
||||||
|
|
||||||
|
|
||||||
async def yamlset_acl(ctx, *, config, update):
|
|
||||||
_fp = io.BytesIO()
|
|
||||||
await ctx.message.attachments[0].save(_fp)
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = yaml.safe_load(_fp)
|
|
||||||
except yaml.YAMLError:
|
|
||||||
_fp.close()
|
|
||||||
del _fp
|
|
||||||
raise
|
|
||||||
|
|
||||||
old_data = await config()
|
|
||||||
|
|
||||||
for outer, inner in data.items():
|
|
||||||
for ok, iv in inner.items():
|
|
||||||
for k, v in iv.items():
|
|
||||||
if k == "default":
|
|
||||||
data[outer][ok][k] = {"allow": True, "deny": False}.get(v.lower(), None)
|
|
||||||
|
|
||||||
if not update:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
if isinstance(old_data[outer][ok][k], list):
|
|
||||||
data[outer][ok][k].extend(old_data[outer][ok][k])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
await config.set(data)
|
|
||||||
|
|
||||||
|
|
||||||
async def yamlget_acl(ctx, *, config):
|
|
||||||
data = await config()
|
|
||||||
removals = []
|
|
||||||
|
|
||||||
for outer, inner in data.items():
|
|
||||||
for ok, iv in inner.items():
|
|
||||||
for k, v in iv.items():
|
|
||||||
if k != "default":
|
|
||||||
continue
|
|
||||||
if v is True:
|
|
||||||
data[outer][ok][k] = "allow"
|
|
||||||
elif v is False:
|
|
||||||
data[outer][ok][k] = "deny"
|
|
||||||
else:
|
|
||||||
removals.append((outer, ok, k))
|
|
||||||
|
|
||||||
for tup in removals:
|
|
||||||
o, i, k = tup
|
|
||||||
data[o][i].pop(k, None)
|
|
||||||
|
|
||||||
_fp = io.BytesIO(yaml.dump(data, default_flow_style=False).encode())
|
|
||||||
_fp.seek(0)
|
|
||||||
await ctx.author.send(file=discord.File(_fp, filename="acl.yaml"))
|
|
||||||
_fp.close()
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Union
|
from typing import Union, List
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from copy import copy
|
from copy import copy
|
||||||
import contextlib
|
import contextlib
|
||||||
@@ -11,6 +11,7 @@ from redbot.core.utils.chat_formatting import pagify, box
|
|||||||
from redbot.core.utils.antispam import AntiSpam
|
from redbot.core.utils.antispam import AntiSpam
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from redbot.core.i18n import Translator, cog_i18n
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
|
from redbot.core.utils.predicates import MessagePredicate
|
||||||
from redbot.core.utils.tunnel import Tunnel
|
from redbot.core.utils.tunnel import Tunnel
|
||||||
|
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ log = logging.getLogger("red.reports")
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Reports:
|
class Reports(commands.Cog):
|
||||||
|
|
||||||
default_guild_settings = {"output_channel": None, "active": False, "next_ticket": 1}
|
default_guild_settings = {"output_channel": None, "active": False, "next_ticket": 1}
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ class Reports:
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = Config.get_conf(self, 78631113035100160, force_registration=True)
|
self.config = Config.get_conf(self, 78631113035100160, force_registration=True)
|
||||||
self.config.register_guild(**self.default_guild_settings)
|
self.config.register_guild(**self.default_guild_settings)
|
||||||
@@ -58,23 +60,20 @@ class Reports:
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.group(name="reportset")
|
@commands.group(name="reportset")
|
||||||
async def reportset(self, ctx: commands.Context):
|
async def reportset(self, ctx: commands.Context):
|
||||||
"""
|
"""Manage Reports."""
|
||||||
Settings for the report system.
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@checks.admin_or_permissions(manage_guild=True)
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
@reportset.command(name="output")
|
@reportset.command(name="output")
|
||||||
async def setoutput(self, ctx: commands.Context, channel: discord.TextChannel):
|
async def reportset_output(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
"""Set the channel where reports will show up"""
|
"""Set the channel where reports will be sent."""
|
||||||
await self.config.guild(ctx.guild).output_channel.set(channel.id)
|
await self.config.guild(ctx.guild).output_channel.set(channel.id)
|
||||||
await ctx.send(_("The report channel has been set."))
|
await ctx.send(_("The report channel has been set."))
|
||||||
|
|
||||||
@checks.admin_or_permissions(manage_guild=True)
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
@reportset.command(name="toggle", aliases=["toggleactive"])
|
@reportset.command(name="toggle", aliases=["toggleactive"])
|
||||||
async def report_toggle(self, ctx: commands.Context):
|
async def reportset_toggle(self, ctx: commands.Context):
|
||||||
"""Enables or Disables reporting for the server"""
|
"""Enable or Disable reporting for this server."""
|
||||||
|
|
||||||
active = await self.config.guild(ctx.guild).active()
|
active = await self.config.guild(ctx.guild).active()
|
||||||
active = not active
|
active = not active
|
||||||
await self.config.guild(ctx.guild).active.set(active)
|
await self.config.guild(ctx.guild).active.set(active)
|
||||||
@@ -87,10 +86,8 @@ class Reports:
|
|||||||
ret = False
|
ret = False
|
||||||
if mod:
|
if mod:
|
||||||
guild = m.guild
|
guild = m.guild
|
||||||
admin_role = discord.utils.get(
|
admin_role = guild.get_role(await self.bot.db.guild(guild).admin_role())
|
||||||
guild.roles, id=await self.bot.db.guild(guild).admin_role()
|
mod_role = guild.get_role(await self.bot.db.guild(guild).mod_role())
|
||||||
)
|
|
||||||
mod_role = discord.utils.get(guild.roles, id=await self.bot.db.guild(guild).mod_role())
|
|
||||||
ret |= any(r in m.roles for r in (mod_role, admin_role))
|
ret |= any(r in m.roles for r in (mod_role, admin_role))
|
||||||
if perms:
|
if perms:
|
||||||
ret |= m.guild_permissions >= perms
|
ret |= m.guild_permissions >= perms
|
||||||
@@ -137,13 +134,14 @@ class Reports:
|
|||||||
output += "\n{}".format(prompt)
|
output += "\n{}".format(prompt)
|
||||||
|
|
||||||
for page in pagify(output, delims=["\n"]):
|
for page in pagify(output, delims=["\n"]):
|
||||||
dm = await author.send(box(page))
|
await author.send(box(page))
|
||||||
|
|
||||||
def pred(m):
|
|
||||||
return m.author == author and m.channel == dm.channel
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = await self.bot.wait_for("message", check=pred, timeout=45)
|
message = await self.bot.wait_for(
|
||||||
|
"message",
|
||||||
|
check=MessagePredicate.same_context(channel=author.dm_channel, user=author),
|
||||||
|
timeout=45,
|
||||||
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await author.send(_("You took too long to select. Try again later."))
|
await author.send(_("You took too long to select. Try again later."))
|
||||||
return None
|
return None
|
||||||
@@ -167,7 +165,7 @@ class Reports:
|
|||||||
if channel is None:
|
if channel is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
files = await Tunnel.files_from_attatch(msg)
|
files: List[discord.File] = await Tunnel.files_from_attatch(msg)
|
||||||
|
|
||||||
ticket_number = await self.config.guild(guild).next_ticket()
|
ticket_number = await self.config.guild(guild).next_ticket()
|
||||||
await self.config.guild(guild).next_ticket.set(ticket_number + 1)
|
await self.config.guild(guild).next_ticket.set(ticket_number + 1)
|
||||||
@@ -203,11 +201,10 @@ class Reports:
|
|||||||
|
|
||||||
@commands.group(name="report", invoke_without_command=True)
|
@commands.group(name="report", invoke_without_command=True)
|
||||||
async def report(self, ctx: commands.Context, *, _report: str = ""):
|
async def report(self, ctx: commands.Context, *, _report: str = ""):
|
||||||
"""
|
"""Send a report.
|
||||||
Send a report.
|
|
||||||
|
|
||||||
Use without arguments for interactive reporting, or do
|
Use without arguments for interactive reporting, or do
|
||||||
[p]report <text> to use it non-interactively.
|
`[p]report <text>` to use it non-interactively.
|
||||||
"""
|
"""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
@@ -248,7 +245,7 @@ class Reports:
|
|||||||
val = await self.send_report(_m, guild)
|
val = await self.send_report(_m, guild)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
dm = await author.send(
|
await author.send(
|
||||||
_(
|
_(
|
||||||
"Please respond to this message with your Report."
|
"Please respond to this message with your Report."
|
||||||
"\nYour report should be a single message"
|
"\nYour report should be a single message"
|
||||||
@@ -257,11 +254,12 @@ class Reports:
|
|||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
return await ctx.send(_("This requires DMs enabled."))
|
return await ctx.send(_("This requires DMs enabled."))
|
||||||
|
|
||||||
def pred(m):
|
|
||||||
return m.author == author and m.channel == dm.channel
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = await self.bot.wait_for("message", check=pred, timeout=180)
|
message = await self.bot.wait_for(
|
||||||
|
"message",
|
||||||
|
check=MessagePredicate.same_context(ctx, channel=author.dm_channel),
|
||||||
|
timeout=180,
|
||||||
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
return await author.send(_("You took too long. Try again later."))
|
return await author.send(_("You took too long. Try again later."))
|
||||||
else:
|
else:
|
||||||
@@ -321,9 +319,8 @@ class Reports:
|
|||||||
@checks.mod_or_permissions(manage_members=True)
|
@checks.mod_or_permissions(manage_members=True)
|
||||||
@report.command(name="interact")
|
@report.command(name="interact")
|
||||||
async def response(self, ctx, ticket_number: int):
|
async def response(self, ctx, ticket_number: int):
|
||||||
"""
|
"""Open a message tunnel.
|
||||||
Open a message tunnel.
|
|
||||||
|
|
||||||
This tunnel will forward things you say in this channel
|
This tunnel will forward things you say in this channel
|
||||||
to the ticket opener's direct messages.
|
to the ticket opener's direct messages.
|
||||||
|
|
||||||
@@ -352,8 +349,7 @@ class Reports:
|
|||||||
)
|
)
|
||||||
|
|
||||||
big_topic = _(
|
big_topic = _(
|
||||||
"{who} opened a 2-way communication "
|
" Anything you say or upload here "
|
||||||
"about ticket number {ticketnum}. Anything you say or upload here "
|
|
||||||
"(8MB file size limitation on uploads) "
|
"(8MB file size limitation on uploads) "
|
||||||
"will be forwarded to them until the communication is closed.\n"
|
"will be forwarded to them until the communication is closed.\n"
|
||||||
"You can close a communication at any point by reacting with "
|
"You can close a communication at any point by reacting with "
|
||||||
@@ -362,8 +358,12 @@ class Reports:
|
|||||||
"\N{WHITE HEAVY CHECK MARK}.\n"
|
"\N{WHITE HEAVY CHECK MARK}.\n"
|
||||||
"Tunnels are not persistent across bot restarts."
|
"Tunnels are not persistent across bot restarts."
|
||||||
)
|
)
|
||||||
topic = big_topic.format(
|
topic = (
|
||||||
ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild)
|
_(
|
||||||
|
"A moderator in the server `{guild.name}` has opened a 2-way communication about "
|
||||||
|
"ticket number {ticket_number}."
|
||||||
|
).format(guild=guild, ticket_number=ticket_number)
|
||||||
|
+ big_topic
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True)
|
m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True)
|
||||||
@@ -371,4 +371,9 @@ class Reports:
|
|||||||
await ctx.send(_("That user has DMs disabled."))
|
await ctx.send(_("That user has DMs disabled."))
|
||||||
else:
|
else:
|
||||||
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m}
|
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m}
|
||||||
await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number))
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"You have opened a 2-way communication about ticket number {ticket_number}."
|
||||||
|
).format(ticket_number=ticket_number)
|
||||||
|
+ big_topic
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import contextlib
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import Config, checks, commands
|
from redbot.core import Config, checks, commands
|
||||||
from redbot.core.utils.chat_formatting import pagify
|
from redbot.core.utils.chat_formatting import pagify
|
||||||
@@ -22,7 +24,7 @@ from .errors import (
|
|||||||
StreamsError,
|
StreamsError,
|
||||||
InvalidTwitchCredentials,
|
InvalidTwitchCredentials,
|
||||||
)
|
)
|
||||||
from . import streamtypes as StreamClasses
|
from . import streamtypes as _streamtypes
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import asyncio
|
import asyncio
|
||||||
import re
|
import re
|
||||||
@@ -35,7 +37,7 @@ _ = Translator("Streams", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Streams:
|
class Streams(commands.Cog):
|
||||||
|
|
||||||
global_defaults = {"tokens": {}, "streams": [], "communities": []}
|
global_defaults = {"tokens": {}, "streams": [], "communities": []}
|
||||||
|
|
||||||
@@ -44,6 +46,7 @@ class Streams:
|
|||||||
role_defaults = {"mention": False}
|
role_defaults = {"mention": False}
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.db = Config.get_conf(self, 26262626)
|
self.db = Config.get_conf(self, 26262626)
|
||||||
|
|
||||||
self.db.register_global(**self.global_defaults)
|
self.db.register_global(**self.global_defaults)
|
||||||
@@ -75,14 +78,14 @@ class Streams:
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def twitch(self, ctx: commands.Context, channel_name: str):
|
async def twitch(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Checks if a Twitch channel is live"""
|
"""Check if a Twitch channel is live."""
|
||||||
token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None)
|
token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None)
|
||||||
stream = TwitchStream(name=channel_name, token=token)
|
stream = TwitchStream(name=channel_name, token=token)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def youtube(self, ctx: commands.Context, channel_id_or_name: str):
|
async def youtube(self, ctx: commands.Context, channel_id_or_name: str):
|
||||||
"""Checks if a Youtube channel is live"""
|
"""Check if a YouTube channel is live."""
|
||||||
apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None)
|
apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None)
|
||||||
is_name = self.check_name_or_id(channel_id_or_name)
|
is_name = self.check_name_or_id(channel_id_or_name)
|
||||||
if is_name:
|
if is_name:
|
||||||
@@ -93,23 +96,24 @@ class Streams:
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def hitbox(self, ctx: commands.Context, channel_name: str):
|
async def hitbox(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Checks if a Hitbox channel is live"""
|
"""Check if a Hitbox channel is live."""
|
||||||
stream = HitboxStream(name=channel_name)
|
stream = HitboxStream(name=channel_name)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def mixer(self, ctx: commands.Context, channel_name: str):
|
async def mixer(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Checks if a Mixer channel is live"""
|
"""Check if a Mixer channel is live."""
|
||||||
stream = MixerStream(name=channel_name)
|
stream = MixerStream(name=channel_name)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def picarto(self, ctx: commands.Context, channel_name: str):
|
async def picarto(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Checks if a Picarto channel is live"""
|
"""Check if a Picarto channel is live."""
|
||||||
stream = PicartoStream(name=channel_name)
|
stream = PicartoStream(name=channel_name)
|
||||||
await self.check_online(ctx, stream)
|
await self.check_online(ctx, stream)
|
||||||
|
|
||||||
async def check_online(self, ctx: commands.Context, stream):
|
@staticmethod
|
||||||
|
async def check_online(ctx: commands.Context, stream):
|
||||||
try:
|
try:
|
||||||
embed = await stream.is_online()
|
embed = await stream.is_online()
|
||||||
except OfflineStream:
|
except OfflineStream:
|
||||||
@@ -118,15 +122,17 @@ class Streams:
|
|||||||
await ctx.send(_("That channel doesn't seem to exist."))
|
await ctx.send(_("That channel doesn't seem to exist."))
|
||||||
except InvalidTwitchCredentials:
|
except InvalidTwitchCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The twitch token is either invalid or has not been set. See `{}`.").format(
|
_(
|
||||||
"{}streamset twitchtoken".format(ctx.prefix)
|
"The Twitch token is either invalid or has not been set. See "
|
||||||
)
|
"`{prefix}streamset twitchtoken`."
|
||||||
|
).format(prefix=ctx.prefix)
|
||||||
)
|
)
|
||||||
except InvalidYoutubeCredentials:
|
except InvalidYoutubeCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Your Youtube API key is either invalid or has not been set. See {}.").format(
|
_(
|
||||||
"`{}streamset youtubekey`".format(ctx.prefix)
|
"The YouTube API key is either invalid or has not been set. See "
|
||||||
)
|
"`{prefix}streamset youtubekey`."
|
||||||
|
).format(prefix=ctx.prefix)
|
||||||
)
|
)
|
||||||
except APIError:
|
except APIError:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
@@ -139,11 +145,12 @@ class Streams:
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.mod()
|
@checks.mod()
|
||||||
async def streamalert(self, ctx: commands.Context):
|
async def streamalert(self, ctx: commands.Context):
|
||||||
|
"""Manage automated stream alerts."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@streamalert.group(name="twitch", invoke_without_command=True)
|
@streamalert.group(name="twitch", invoke_without_command=True)
|
||||||
async def _twitch(self, ctx: commands.Context, channel_name: str = None):
|
async def _twitch(self, ctx: commands.Context, channel_name: str = None):
|
||||||
"""Twitch stream alerts"""
|
"""Manage Twitch stream notifications."""
|
||||||
if channel_name is not None:
|
if channel_name is not None:
|
||||||
await ctx.invoke(self.twitch_alert_channel, channel_name)
|
await ctx.invoke(self.twitch_alert_channel, channel_name)
|
||||||
else:
|
else:
|
||||||
@@ -151,7 +158,7 @@ class Streams:
|
|||||||
|
|
||||||
@_twitch.command(name="channel")
|
@_twitch.command(name="channel")
|
||||||
async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str):
|
async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Sets a Twitch alert notification in the channel"""
|
"""Toggle alerts in this channel for a Twitch stream."""
|
||||||
if re.fullmatch(r"<#\d+>", channel_name):
|
if re.fullmatch(r"<#\d+>", channel_name):
|
||||||
await ctx.send("Please supply the name of a *Twitch* channel, not a Discord channel.")
|
await ctx.send("Please supply the name of a *Twitch* channel, not a Discord channel.")
|
||||||
return
|
return
|
||||||
@@ -159,33 +166,39 @@ class Streams:
|
|||||||
|
|
||||||
@_twitch.command(name="community")
|
@_twitch.command(name="community")
|
||||||
async def twitch_alert_community(self, ctx: commands.Context, community: str):
|
async def twitch_alert_community(self, ctx: commands.Context, community: str):
|
||||||
"""Sets an alert notification in the channel for the specified twitch community."""
|
"""Toggle alerts in this channel for a Twitch community."""
|
||||||
await self.community_alert(ctx, TwitchCommunity, community.lower())
|
await self.community_alert(ctx, TwitchCommunity, community.lower())
|
||||||
|
|
||||||
@streamalert.command(name="youtube")
|
@streamalert.command(name="youtube")
|
||||||
async def youtube_alert(self, ctx: commands.Context, channel_name_or_id: str):
|
async def youtube_alert(self, ctx: commands.Context, channel_name_or_id: str):
|
||||||
"""Sets a Youtube alert notification in the channel"""
|
"""Toggle alerts in this channel for a YouTube stream."""
|
||||||
await self.stream_alert(ctx, YoutubeStream, channel_name_or_id)
|
await self.stream_alert(ctx, YoutubeStream, channel_name_or_id)
|
||||||
|
|
||||||
@streamalert.command(name="hitbox")
|
@streamalert.command(name="hitbox")
|
||||||
async def hitbox_alert(self, ctx: commands.Context, channel_name: str):
|
async def hitbox_alert(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Sets a Hitbox alert notification in the channel"""
|
"""Toggle alerts in this channel for a Hitbox stream."""
|
||||||
await self.stream_alert(ctx, HitboxStream, channel_name)
|
await self.stream_alert(ctx, HitboxStream, channel_name)
|
||||||
|
|
||||||
@streamalert.command(name="mixer")
|
@streamalert.command(name="mixer")
|
||||||
async def mixer_alert(self, ctx: commands.Context, channel_name: str):
|
async def mixer_alert(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Sets a Mixer alert notification in the channel"""
|
"""Toggle alerts in this channel for a Mixer stream."""
|
||||||
await self.stream_alert(ctx, MixerStream, channel_name)
|
await self.stream_alert(ctx, MixerStream, channel_name)
|
||||||
|
|
||||||
@streamalert.command(name="picarto")
|
@streamalert.command(name="picarto")
|
||||||
async def picarto_alert(self, ctx: commands.Context, channel_name: str):
|
async def picarto_alert(self, ctx: commands.Context, channel_name: str):
|
||||||
"""Sets a Picarto alert notification in the channel"""
|
"""Toggle alerts in this channel for a Picarto stream."""
|
||||||
await self.stream_alert(ctx, PicartoStream, channel_name)
|
await self.stream_alert(ctx, PicartoStream, channel_name)
|
||||||
|
|
||||||
@streamalert.command(name="stop")
|
@streamalert.command(name="stop", usage="[disable_all=No]")
|
||||||
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
|
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
|
||||||
"""Stops all stream notifications in the channel
|
"""Disable all stream alerts in this channel or server.
|
||||||
Adding 'yes' will disable all notifications in the server"""
|
|
||||||
|
`[p]streamalert stop` will disable this channel's stream
|
||||||
|
alerts.
|
||||||
|
|
||||||
|
Do `[p]streamalert stop yes` to disable all stream alerts in
|
||||||
|
this server.
|
||||||
|
"""
|
||||||
streams = self.streams.copy()
|
streams = self.streams.copy()
|
||||||
local_channel_ids = [c.id for c in ctx.guild.channels]
|
local_channel_ids = [c.id for c in ctx.guild.channels]
|
||||||
to_remove = []
|
to_remove = []
|
||||||
@@ -207,9 +220,10 @@ class Streams:
|
|||||||
self.streams = streams
|
self.streams = streams
|
||||||
await self.save_streams()
|
await self.save_streams()
|
||||||
|
|
||||||
msg = _("All the alerts in the {} have been disabled.").format(
|
if _all:
|
||||||
"server" if _all else "channel"
|
msg = _("All the stream alerts in this server have been disabled.")
|
||||||
)
|
else:
|
||||||
|
msg = _("All the stream alerts in this channel have been disabled.")
|
||||||
|
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
|
|
||||||
@@ -249,16 +263,18 @@ class Streams:
|
|||||||
exists = await self.check_exists(stream)
|
exists = await self.check_exists(stream)
|
||||||
except InvalidTwitchCredentials:
|
except InvalidTwitchCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Your twitch token is either invalid or has not been set. See {}.").format(
|
_(
|
||||||
"`{}streamset twitchtoken`".format(ctx.prefix)
|
"The Twitch token is either invalid or has not been set. See "
|
||||||
)
|
"`{prefix}streamset twitchtoken`."
|
||||||
|
).format(prefix=ctx.prefix)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except InvalidYoutubeCredentials:
|
except InvalidYoutubeCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"Your Youtube API key is either invalid or has not been set. See {}."
|
"The YouTube API key is either invalid or has not been set. See "
|
||||||
).format("`{}streamset youtubekey`".format(ctx.prefix))
|
"`{prefix}streamset youtubekey`."
|
||||||
|
).format(prefix=ctx.prefix)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except APIError:
|
except APIError:
|
||||||
@@ -282,9 +298,10 @@ class Streams:
|
|||||||
await community.get_community_streams()
|
await community.get_community_streams()
|
||||||
except InvalidTwitchCredentials:
|
except InvalidTwitchCredentials:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("The twitch token is either invalid or has not been set. See {}.").format(
|
_(
|
||||||
"`{}streamset twitchtoken`".format(ctx.prefix)
|
"The Twitch token is either invalid or has not been set. See "
|
||||||
)
|
"`{prefix}streamset twitchtoken`."
|
||||||
|
).format(prefix=ctx.prefix)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except CommunityNotFound:
|
except CommunityNotFound:
|
||||||
@@ -308,14 +325,15 @@ class Streams:
|
|||||||
@streamset.command()
|
@streamset.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def twitchtoken(self, ctx: commands.Context, token: str):
|
async def twitchtoken(self, ctx: commands.Context, token: str):
|
||||||
"""Set the Client ID for twitch.
|
"""Set the Client ID for Twitch.
|
||||||
|
|
||||||
To do this, follow these steps:
|
To do this, follow these steps:
|
||||||
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
|
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
|
||||||
2. Click *Register Your Application*
|
2. Click *Register Your Application*
|
||||||
3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and
|
3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and
|
||||||
select an Application Category of your choosing.
|
select an Application Category of your choosing.
|
||||||
4. Click *Register*, and on the following page, copy the Client ID.
|
4. Click *Register*, and on the following page, copy the Client ID.
|
||||||
5. Paste the Client ID into this command. Done!
|
5. Paste the Client ID into this command. Done!
|
||||||
"""
|
"""
|
||||||
await self.db.tokens.set_raw("TwitchStream", value=token)
|
await self.db.tokens.set_raw("TwitchStream", value=token)
|
||||||
await self.db.tokens.set_raw("TwitchCommunity", value=token)
|
await self.db.tokens.set_raw("TwitchCommunity", value=token)
|
||||||
@@ -324,64 +342,59 @@ class Streams:
|
|||||||
@streamset.command()
|
@streamset.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def youtubekey(self, ctx: commands.Context, key: str):
|
async def youtubekey(self, ctx: commands.Context, key: str):
|
||||||
"""Sets the API key for Youtube.
|
"""Set the API key for YouTube.
|
||||||
|
|
||||||
To get one, do the following:
|
To get one, do the following:
|
||||||
1. Create a project (see https://support.google.com/googleapi/answer/6251787 for details)
|
1. Create a project (see https://support.google.com/googleapi/answer/6251787 for details)
|
||||||
2. Enable the Youtube Data API v3 (see https://support.google.com/googleapi/answer/6158841 for instructions)
|
2. Enable the YouTube Data API v3 (see https://support.google.com/googleapi/answer/6158841
|
||||||
3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for instructions)
|
for instructions)
|
||||||
|
3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for
|
||||||
|
instructions)
|
||||||
4. Copy your API key and paste it into this command. Done!
|
4. Copy your API key and paste it into this command. Done!
|
||||||
"""
|
"""
|
||||||
await self.db.tokens.set_raw("YoutubeStream", value=key)
|
await self.db.tokens.set_raw("YoutubeStream", value=key)
|
||||||
await ctx.send(_("Youtube key set."))
|
await ctx.send(_("YouTube key set."))
|
||||||
|
|
||||||
@streamset.group()
|
@streamset.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def mention(self, ctx: commands.Context):
|
async def mention(self, ctx: commands.Context):
|
||||||
"""Sets mentions for alerts."""
|
"""Manage mention settings for stream alerts."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@mention.command(aliases=["everyone"])
|
@mention.command(aliases=["everyone"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def all(self, ctx: commands.Context):
|
async def all(self, ctx: commands.Context):
|
||||||
"""Toggles everyone mention"""
|
"""Toggle the `@\u200beveryone` mention."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
current_setting = await self.db.guild(guild).mention_everyone()
|
current_setting = await self.db.guild(guild).mention_everyone()
|
||||||
if current_setting:
|
if current_setting:
|
||||||
await self.db.guild(guild).mention_everyone.set(False)
|
await self.db.guild(guild).mention_everyone.set(False)
|
||||||
await ctx.send(
|
await ctx.send(_("`@\u200beveryone` will no longer be mentioned for stream alerts."))
|
||||||
_("{} will no longer be mentioned when a stream or community is live").format(
|
|
||||||
"@\u200beveryone"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await self.db.guild(guild).mention_everyone.set(True)
|
await self.db.guild(guild).mention_everyone.set(True)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("When a stream or community " "is live, {} will be mentioned.").format(
|
_("When a stream or community is live, `@\u200beveryone` will be mentioned.")
|
||||||
"@\u200beveryone"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@mention.command(aliases=["here"])
|
@mention.command(aliases=["here"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def online(self, ctx: commands.Context):
|
async def online(self, ctx: commands.Context):
|
||||||
"""Toggles here mention"""
|
"""Toggle the `@\u200bhere` mention."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
current_setting = await self.db.guild(guild).mention_here()
|
current_setting = await self.db.guild(guild).mention_here()
|
||||||
if current_setting:
|
if current_setting:
|
||||||
await self.db.guild(guild).mention_here.set(False)
|
await self.db.guild(guild).mention_here.set(False)
|
||||||
await ctx.send(_("{} will no longer be mentioned for an alert.").format("@\u200bhere"))
|
await ctx.send(_("`@\u200bhere` will no longer be mentioned for stream alerts."))
|
||||||
else:
|
else:
|
||||||
await self.db.guild(guild).mention_here.set(True)
|
await self.db.guild(guild).mention_here.set(True)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("When a stream or community " "is live, {} will be mentioned.").format(
|
_("When a stream or community is live, `@\u200bhere` will be mentioned.")
|
||||||
"@\u200bhere"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@mention.command()
|
@mention.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def role(self, ctx: commands.Context, *, role: discord.Role):
|
async def role(self, ctx: commands.Context, *, role: discord.Role):
|
||||||
"""Toggles role mention"""
|
"""Toggle a role mention."""
|
||||||
current_setting = await self.db.role(role).mention()
|
current_setting = await self.db.role(role).mention()
|
||||||
if not role.mentionable:
|
if not role.mentionable:
|
||||||
await ctx.send("That role is not mentionable!")
|
await ctx.send("That role is not mentionable!")
|
||||||
@@ -389,27 +402,27 @@ class Streams:
|
|||||||
if current_setting:
|
if current_setting:
|
||||||
await self.db.role(role).mention.set(False)
|
await self.db.role(role).mention.set(False)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("{} will no longer be mentioned for an alert.").format(
|
_("`@\u200b{role.name}` will no longer be mentioned for stream alerts.").format(
|
||||||
"@\u200b{}".format(role.name)
|
role=role
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await self.db.role(role).mention.set(True)
|
await self.db.role(role).mention.set(True)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("When a stream or community " "is live, {} will be mentioned." "").format(
|
_(
|
||||||
"@\u200b{}".format(role.name)
|
"When a stream or community is live, `@\u200b{role.name}` will be mentioned."
|
||||||
)
|
).format(role=role)
|
||||||
)
|
)
|
||||||
|
|
||||||
@streamset.command()
|
@streamset.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def autodelete(self, ctx: commands.Context, on_off: bool):
|
async def autodelete(self, ctx: commands.Context, on_off: bool):
|
||||||
"""Toggles automatic deletion of notifications for streams that go offline"""
|
"""Toggle alert deletion for when streams go offline."""
|
||||||
await self.db.guild(ctx.guild).autodelete.set(on_off)
|
await self.db.guild(ctx.guild).autodelete.set(on_off)
|
||||||
if on_off:
|
if on_off:
|
||||||
await ctx.send("The notifications will be deleted once streams go offline.")
|
await ctx.send(_("The notifications will be deleted once streams go offline."))
|
||||||
else:
|
else:
|
||||||
await ctx.send("Notifications will never be deleted.")
|
await ctx.send(_("Notifications will no longer be deleted."))
|
||||||
|
|
||||||
async def add_or_remove(self, ctx: commands.Context, stream):
|
async def add_or_remove(self, ctx: commands.Context, stream):
|
||||||
if ctx.channel.id not in stream.channels:
|
if ctx.channel.id not in stream.channels:
|
||||||
@@ -417,18 +430,18 @@ class Streams:
|
|||||||
if stream not in self.streams:
|
if stream not in self.streams:
|
||||||
self.streams.append(stream)
|
self.streams.append(stream)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("I'll now send a notification in this channel when {} is live.").format(
|
_(
|
||||||
stream.name
|
"I'll now send a notification in this channel when {stream.name} is live."
|
||||||
)
|
).format(stream=stream)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
stream.channels.remove(ctx.channel.id)
|
stream.channels.remove(ctx.channel.id)
|
||||||
if not stream.channels:
|
if not stream.channels:
|
||||||
self.streams.remove(stream)
|
self.streams.remove(stream)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("I won't send notifications about {} in this channel anymore.").format(
|
_(
|
||||||
stream.name
|
"I won't send notifications about {stream.name} in this channel anymore."
|
||||||
)
|
).format(stream=stream)
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.save_streams()
|
await self.save_streams()
|
||||||
@@ -441,9 +454,8 @@ class Streams:
|
|||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"I'll send a notification in this channel when a "
|
"I'll send a notification in this channel when a "
|
||||||
"channel is live in the {} community."
|
"channel is live in the {community.name} community."
|
||||||
""
|
).format(community=community)
|
||||||
).format(community.name)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
community.channels.remove(ctx.channel.id)
|
community.channels.remove(ctx.channel.id)
|
||||||
@@ -452,9 +464,8 @@ class Streams:
|
|||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"I won't send notifications about channels streaming "
|
"I won't send notifications about channels streaming "
|
||||||
"in the {} community in this channel anymore."
|
"in the {community.name} community in this channel anymore."
|
||||||
""
|
).format(community=community)
|
||||||
).format(community.name)
|
|
||||||
)
|
)
|
||||||
await self.save_communities()
|
await self.save_communities()
|
||||||
|
|
||||||
@@ -480,7 +491,8 @@ class Streams:
|
|||||||
if community.type == _class.__name__ and community.name.lower() == name.lower():
|
if community.type == _class.__name__ and community.name.lower() == name.lower():
|
||||||
return community
|
return community
|
||||||
|
|
||||||
async def check_exists(self, stream):
|
@staticmethod
|
||||||
|
async def check_exists(stream):
|
||||||
try:
|
try:
|
||||||
await stream.is_online()
|
await stream.is_online()
|
||||||
except OfflineStream:
|
except OfflineStream:
|
||||||
@@ -505,40 +517,36 @@ class Streams:
|
|||||||
|
|
||||||
async def check_streams(self):
|
async def check_streams(self):
|
||||||
for stream in self.streams:
|
for stream in self.streams:
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
embed = await stream.is_online()
|
try:
|
||||||
except OfflineStream:
|
embed = await stream.is_online()
|
||||||
if not stream._messages_cache:
|
except OfflineStream:
|
||||||
continue
|
if not stream._messages_cache:
|
||||||
for message in stream._messages_cache:
|
continue
|
||||||
try:
|
for message in stream._messages_cache:
|
||||||
autodelete = await self.db.guild(message.guild).autodelete()
|
with contextlib.suppress(Exception):
|
||||||
if autodelete:
|
autodelete = await self.db.guild(message.guild).autodelete()
|
||||||
await message.delete()
|
if autodelete:
|
||||||
except:
|
await message.delete()
|
||||||
pass
|
stream._messages_cache.clear()
|
||||||
stream._messages_cache.clear()
|
await self.save_streams()
|
||||||
await self.save_streams()
|
else:
|
||||||
except:
|
if stream._messages_cache:
|
||||||
pass
|
continue
|
||||||
else:
|
for channel_id in stream.channels:
|
||||||
if stream._messages_cache:
|
channel = self.bot.get_channel(channel_id)
|
||||||
continue
|
mention_str = await self._get_mention_str(channel.guild)
|
||||||
for channel_id in stream.channels:
|
|
||||||
channel = self.bot.get_channel(channel_id)
|
|
||||||
mention_str = await self._get_mention_str(channel.guild)
|
|
||||||
|
|
||||||
if mention_str:
|
if mention_str:
|
||||||
content = "{}, {} is live!".format(mention_str, stream.name)
|
content = _("{mention}, {stream.name} is live!").format(
|
||||||
else:
|
mention=mention_str, stream=stream
|
||||||
content = "{} is live!".format(stream.name)
|
)
|
||||||
|
else:
|
||||||
|
content = _("{stream.name} is live!").format(stream=stream.name)
|
||||||
|
|
||||||
try:
|
|
||||||
m = await channel.send(content, embed=embed)
|
m = await channel.send(content, embed=embed)
|
||||||
stream._messages_cache.append(m)
|
stream._messages_cache.append(m)
|
||||||
await self.save_streams()
|
await self.save_streams()
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def _get_mention_str(self, guild: discord.Guild):
|
async def _get_mention_str(self, guild: discord.Guild):
|
||||||
settings = self.db.guild(guild)
|
settings = self.db.guild(guild)
|
||||||
@@ -554,45 +562,46 @@ class Streams:
|
|||||||
|
|
||||||
async def check_communities(self):
|
async def check_communities(self):
|
||||||
for community in self.communities:
|
for community in self.communities:
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
stream_list = await community.get_community_streams()
|
try:
|
||||||
except CommunityNotFound:
|
stream_list = await community.get_community_streams()
|
||||||
print(_("The Community {} was not found!").format(community.name))
|
except CommunityNotFound:
|
||||||
continue
|
print(
|
||||||
except OfflineCommunity:
|
_("The Community {community.name} was not found!").format(
|
||||||
if not community._messages_cache:
|
community=community
|
||||||
|
)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
for message in community._messages_cache:
|
except OfflineCommunity:
|
||||||
try:
|
if not community._messages_cache:
|
||||||
autodelete = await self.db.guild(message.guild).autodelete()
|
continue
|
||||||
if autodelete:
|
for message in community._messages_cache:
|
||||||
await message.delete()
|
with contextlib.suppress(Exception):
|
||||||
except:
|
autodelete = await self.db.guild(message.guild).autodelete()
|
||||||
pass
|
if autodelete:
|
||||||
community._messages_cache.clear()
|
await message.delete()
|
||||||
await self.save_communities()
|
community._messages_cache.clear()
|
||||||
except:
|
await self.save_communities()
|
||||||
pass
|
else:
|
||||||
else:
|
for channel in community.channels:
|
||||||
for channel in community.channels:
|
chn = self.bot.get_channel(channel)
|
||||||
chn = self.bot.get_channel(channel)
|
streams = await self.filter_streams(stream_list, chn)
|
||||||
streams = await self.filter_streams(stream_list, chn)
|
emb = await community.make_embed(streams)
|
||||||
emb = await community.make_embed(streams)
|
chn_msg = [m for m in community._messages_cache if m.channel == chn]
|
||||||
chn_msg = [m for m in community._messages_cache if m.channel == chn]
|
if not chn_msg:
|
||||||
if not chn_msg:
|
mentions = await self._get_mention_str(chn.guild)
|
||||||
mentions = await self._get_mention_str(chn.guild)
|
if mentions:
|
||||||
if mentions:
|
msg = await chn.send(mentions, embed=emb)
|
||||||
msg = await chn.send(mentions, embed=emb)
|
else:
|
||||||
|
msg = await chn.send(embed=emb)
|
||||||
|
community._messages_cache.append(msg)
|
||||||
|
await self.save_communities()
|
||||||
else:
|
else:
|
||||||
msg = await chn.send(embed=emb)
|
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
|
||||||
community._messages_cache.append(msg)
|
community._messages_cache.remove(chn_msg)
|
||||||
await self.save_communities()
|
await chn_msg.edit(embed=emb)
|
||||||
else:
|
community._messages_cache.append(chn_msg)
|
||||||
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
|
await self.save_communities()
|
||||||
community._messages_cache.remove(chn_msg)
|
|
||||||
await chn_msg.edit(embed=emb)
|
|
||||||
community._messages_cache.append(chn_msg)
|
|
||||||
await self.save_communities()
|
|
||||||
|
|
||||||
async def filter_streams(self, streams: list, channel: discord.TextChannel) -> list:
|
async def filter_streams(self, streams: list, channel: discord.TextChannel) -> list:
|
||||||
filtered = []
|
filtered = []
|
||||||
@@ -610,7 +619,7 @@ class Streams:
|
|||||||
streams = []
|
streams = []
|
||||||
|
|
||||||
for raw_stream in await self.db.streams():
|
for raw_stream in await self.db.streams():
|
||||||
_class = getattr(StreamClasses, raw_stream["type"], None)
|
_class = getattr(_streamtypes, raw_stream["type"], None)
|
||||||
if not _class:
|
if not _class:
|
||||||
continue
|
continue
|
||||||
raw_msg_cache = raw_stream["messages"]
|
raw_msg_cache = raw_stream["messages"]
|
||||||
@@ -630,7 +639,7 @@ class Streams:
|
|||||||
communities = []
|
communities = []
|
||||||
|
|
||||||
for raw_community in await self.db.communities():
|
for raw_community in await self.db.communities():
|
||||||
_class = getattr(StreamClasses, raw_community["type"], None)
|
_class = getattr(_streamtypes, raw_community["type"], None)
|
||||||
if not _class:
|
if not _class:
|
||||||
continue
|
continue
|
||||||
raw_msg_cache = raw_community["messages"]
|
raw_msg_cache = raw_community["messages"]
|
||||||
@@ -666,3 +675,5 @@ class Streams:
|
|||||||
def __unload(self):
|
def __unload(self):
|
||||||
if self.task:
|
if self.task:
|
||||||
self.task.cancel()
|
self.task.cancel()
|
||||||
|
|
||||||
|
__del__ = __unload
|
||||||
|
|||||||
@@ -4,19 +4,30 @@ import time
|
|||||||
import random
|
import random
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
import discord
|
import discord
|
||||||
from redbot.core.bank import deposit_credits
|
from redbot.core import bank
|
||||||
from redbot.core.utils.chat_formatting import box
|
from redbot.core.i18n import Translator
|
||||||
|
from redbot.core.utils.chat_formatting import box, bold, humanize_list
|
||||||
|
from redbot.core.utils.common_filters import normalize_smartquotes
|
||||||
from .log import LOG
|
from .log import LOG
|
||||||
|
|
||||||
__all__ = ["TriviaSession"]
|
__all__ = ["TriviaSession"]
|
||||||
|
|
||||||
_REVEAL_MESSAGES = ("I know this one! {}!", "Easy: {}.", "Oh really? It's {} of course.")
|
T_ = Translator("TriviaSession", __file__)
|
||||||
_FAIL_MESSAGES = (
|
|
||||||
"To the next one I guess...",
|
|
||||||
"Moving on...",
|
_ = lambda s: s
|
||||||
"I'm sure you'll know the answer of the next one.",
|
_REVEAL_MESSAGES = (
|
||||||
"\N{PENSIVE FACE} Next one.",
|
_("I know this one! {answer}!"),
|
||||||
|
_("Easy: {answer}."),
|
||||||
|
_("Oh really? It's {answer} of course."),
|
||||||
)
|
)
|
||||||
|
_FAIL_MESSAGES = (
|
||||||
|
_("To the next one I guess..."),
|
||||||
|
_("Moving on..."),
|
||||||
|
_("I'm sure you'll know the answer of the next one."),
|
||||||
|
_("\N{PENSIVE FACE} Next one."),
|
||||||
|
)
|
||||||
|
_ = T_
|
||||||
|
|
||||||
|
|
||||||
class TriviaSession:
|
class TriviaSession:
|
||||||
@@ -103,7 +114,7 @@ class TriviaSession:
|
|||||||
async with self.ctx.typing():
|
async with self.ctx.typing():
|
||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
self.count += 1
|
self.count += 1
|
||||||
msg = "**Question number {}!**\n\n{}".format(self.count, question)
|
msg = bold(_("**Question number {num}!").format(num=self.count)) + "\n\n" + question
|
||||||
await self.ctx.send(msg)
|
await self.ctx.send(msg)
|
||||||
continue_ = await self.wait_for_answer(answers, delay, timeout)
|
continue_ = await self.wait_for_answer(answers, delay, timeout)
|
||||||
if continue_ is False:
|
if continue_ is False:
|
||||||
@@ -112,7 +123,7 @@ class TriviaSession:
|
|||||||
await self.end_game()
|
await self.end_game()
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
await self.ctx.send("There are no more questions!")
|
await self.ctx.send(_("There are no more questions!"))
|
||||||
await self.end_game()
|
await self.end_game()
|
||||||
|
|
||||||
async def _send_startup_msg(self):
|
async def _send_startup_msg(self):
|
||||||
@@ -120,20 +131,13 @@ class TriviaSession:
|
|||||||
for idx, tup in enumerate(self.settings["lists"].items()):
|
for idx, tup in enumerate(self.settings["lists"].items()):
|
||||||
name, author = tup
|
name, author = tup
|
||||||
if author:
|
if author:
|
||||||
title = "{} (by {})".format(name, author)
|
title = _("{trivia_list} (by {author})").format(trivia_list=name, author=author)
|
||||||
else:
|
else:
|
||||||
title = name
|
title = name
|
||||||
list_names.append(title)
|
list_names.append(title)
|
||||||
num_lists = len(list_names)
|
await self.ctx.send(
|
||||||
if num_lists > 2:
|
_("Starting Trivia: {list_names}").format(list_names=humanize_list(list_names))
|
||||||
# at least 3 lists, join all but last with comma
|
)
|
||||||
msg = ", ".join(list_names[: num_lists - 1])
|
|
||||||
# join onto last with "and"
|
|
||||||
msg = " and ".join((msg, list_names[num_lists - 1]))
|
|
||||||
else:
|
|
||||||
# either 1 or 2 lists, join together with "and"
|
|
||||||
msg = " and ".join(list_names)
|
|
||||||
await self.ctx.send("Starting Trivia: " + msg)
|
|
||||||
|
|
||||||
def _iter_questions(self):
|
def _iter_questions(self):
|
||||||
"""Iterate over questions and answers for this session.
|
"""Iterate over questions and answers for this session.
|
||||||
@@ -178,20 +182,20 @@ class TriviaSession:
|
|||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
if time.time() - self._last_response >= timeout:
|
if time.time() - self._last_response >= timeout:
|
||||||
await self.ctx.send("Guys...? Well, I guess I'll stop then.")
|
await self.ctx.send(_("Guys...? Well, I guess I'll stop then."))
|
||||||
self.stop()
|
self.stop()
|
||||||
return False
|
return False
|
||||||
if self.settings["reveal_answer"]:
|
if self.settings["reveal_answer"]:
|
||||||
reply = random.choice(_REVEAL_MESSAGES).format(answers[0])
|
reply = T_(random.choice(_REVEAL_MESSAGES)).format(answer=answers[0])
|
||||||
else:
|
else:
|
||||||
reply = random.choice(_FAIL_MESSAGES)
|
reply = T_(random.choice(_FAIL_MESSAGES))
|
||||||
if self.settings["bot_plays"]:
|
if self.settings["bot_plays"]:
|
||||||
reply += " **+1** for me!"
|
reply += _(" **+1** for me!")
|
||||||
self.scores[self.ctx.guild.me] += 1
|
self.scores[self.ctx.guild.me] += 1
|
||||||
await self.ctx.send(reply)
|
await self.ctx.send(reply)
|
||||||
else:
|
else:
|
||||||
self.scores[message.author] += 1
|
self.scores[message.author] += 1
|
||||||
reply = "You got it {}! **+1** to you!".format(message.author.display_name)
|
reply = _("You got it {user}! **+1** to you!").format(user=message.author.display_name)
|
||||||
await self.ctx.send(reply)
|
await self.ctx.send(reply)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -222,6 +226,7 @@ class TriviaSession:
|
|||||||
|
|
||||||
self._last_response = time.time()
|
self._last_response = time.time()
|
||||||
guess = message.content.lower()
|
guess = message.content.lower()
|
||||||
|
guess = normalize_smartquotes(guess)
|
||||||
for answer in answers:
|
for answer in answers:
|
||||||
if " " in answer and answer in guess:
|
if " " in answer and answer in guess:
|
||||||
# Exact matching, issue #331
|
# Exact matching, issue #331
|
||||||
@@ -280,10 +285,16 @@ class TriviaSession:
|
|||||||
amount = int(multiplier * score)
|
amount = int(multiplier * score)
|
||||||
if amount > 0:
|
if amount > 0:
|
||||||
LOG.debug("Paying trivia winner: %d credits --> %s", amount, str(winner))
|
LOG.debug("Paying trivia winner: %d credits --> %s", amount, str(winner))
|
||||||
await deposit_credits(winner, int(multiplier * score))
|
await bank.deposit_credits(winner, int(multiplier * score))
|
||||||
await self.ctx.send(
|
await self.ctx.send(
|
||||||
"Congratulations, {0}, you have received {1} credits"
|
_(
|
||||||
" for coming first.".format(winner.display_name, amount)
|
"Congratulations, {user}, you have received {num} {currency}"
|
||||||
|
" for coming first."
|
||||||
|
).format(
|
||||||
|
user=winner.display_name,
|
||||||
|
num=amount,
|
||||||
|
currency=await bank.get_currency_name(self.ctx.guild),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -311,9 +322,9 @@ def _parse_answers(answers):
|
|||||||
for answer in answers:
|
for answer in answers:
|
||||||
if isinstance(answer, bool):
|
if isinstance(answer, bool):
|
||||||
if answer is True:
|
if answer is True:
|
||||||
ret.append("True", "Yes")
|
ret.extend(["True", "Yes", _("Yes")])
|
||||||
else:
|
else:
|
||||||
ret.append("False", "No")
|
ret.extend(["False", "No", _("No")])
|
||||||
else:
|
else:
|
||||||
ret.append(str(answer))
|
ret.append(str(answer))
|
||||||
# Uniquify list
|
# Uniquify list
|
||||||
|
|||||||
@@ -7,14 +7,17 @@ import discord
|
|||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
from redbot.core import Config, checks
|
from redbot.core import Config, checks
|
||||||
from redbot.core.data_manager import cog_data_path
|
from redbot.core.data_manager import cog_data_path
|
||||||
from redbot.core.utils.chat_formatting import box, pagify
|
from redbot.core.i18n import Translator, cog_i18n
|
||||||
|
from redbot.core.utils.chat_formatting import box, pagify, bold
|
||||||
from redbot.cogs.bank import check_global_setting_admin
|
from redbot.cogs.bank import check_global_setting_admin
|
||||||
from .log import LOG
|
from .log import LOG
|
||||||
from .session import TriviaSession
|
from .session import TriviaSession
|
||||||
|
|
||||||
__all__ = ["Trivia", "UNIQUE_ID", "get_core_lists"]
|
__all__ = ["Trivia", "UNIQUE_ID", "get_core_lists"]
|
||||||
|
|
||||||
UNIQUE_ID = 0xb3c0e453
|
UNIQUE_ID = 0xB3C0E453
|
||||||
|
|
||||||
|
_ = Translator("Trivia", __file__)
|
||||||
|
|
||||||
|
|
||||||
class InvalidListError(Exception):
|
class InvalidListError(Exception):
|
||||||
@@ -23,10 +26,12 @@ class InvalidListError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Trivia:
|
@cog_i18n(_)
|
||||||
|
class Trivia(commands.Cog):
|
||||||
"""Play trivia with friends!"""
|
"""Play trivia with friends!"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self.trivia_sessions = []
|
self.trivia_sessions = []
|
||||||
self.conf = Config.get_conf(self, identifier=UNIQUE_ID, force_registration=True)
|
self.conf = Config.get_conf(self, identifier=UNIQUE_ID, force_registration=True)
|
||||||
|
|
||||||
@@ -46,20 +51,21 @@ class Trivia:
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def triviaset(self, ctx: commands.Context):
|
async def triviaset(self, ctx: commands.Context):
|
||||||
"""Manage trivia settings."""
|
"""Manage Trivia settings."""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
settings = self.conf.guild(ctx.guild)
|
settings = self.conf.guild(ctx.guild)
|
||||||
settings_dict = await settings.all()
|
settings_dict = await settings.all()
|
||||||
msg = box(
|
msg = box(
|
||||||
"**Current settings**\n"
|
_(
|
||||||
"Bot gains points: {bot_plays}\n"
|
"**Current settings**\n"
|
||||||
"Answer time limit: {delay} seconds\n"
|
"Bot gains points: {bot_plays}\n"
|
||||||
"Lack of response timeout: {timeout} seconds\n"
|
"Answer time limit: {delay} seconds\n"
|
||||||
"Points to win: {max_score}\n"
|
"Lack of response timeout: {timeout} seconds\n"
|
||||||
"Reveal answer on timeout: {reveal_answer}\n"
|
"Points to win: {max_score}\n"
|
||||||
"Payout multiplier: {payout_multiplier}\n"
|
"Reveal answer on timeout: {reveal_answer}\n"
|
||||||
"Allow lists to override settings: {allow_override}"
|
"Payout multiplier: {payout_multiplier}\n"
|
||||||
"".format(**settings_dict),
|
"Allow lists to override settings: {allow_override}"
|
||||||
|
).format(**settings_dict),
|
||||||
lang="py",
|
lang="py",
|
||||||
)
|
)
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
@@ -68,33 +74,34 @@ class Trivia:
|
|||||||
async def triviaset_max_score(self, ctx: commands.Context, score: int):
|
async def triviaset_max_score(self, ctx: commands.Context, score: int):
|
||||||
"""Set the total points required to win."""
|
"""Set the total points required to win."""
|
||||||
if score < 0:
|
if score < 0:
|
||||||
await ctx.send("Score must be greater than 0.")
|
await ctx.send(_("Score must be greater than 0."))
|
||||||
return
|
return
|
||||||
settings = self.conf.guild(ctx.guild)
|
settings = self.conf.guild(ctx.guild)
|
||||||
await settings.max_score.set(score)
|
await settings.max_score.set(score)
|
||||||
await ctx.send("Done. Points required to win set to {}.".format(score))
|
await ctx.send(_("Done. Points required to win set to {num}.").format(num=score))
|
||||||
|
|
||||||
@triviaset.command(name="timelimit")
|
@triviaset.command(name="timelimit")
|
||||||
async def triviaset_timelimit(self, ctx: commands.Context, seconds: float):
|
async def triviaset_timelimit(self, ctx: commands.Context, seconds: float):
|
||||||
"""Set the maximum seconds permitted to answer a question."""
|
"""Set the maximum seconds permitted to answer a question."""
|
||||||
if seconds < 4.0:
|
if seconds < 4.0:
|
||||||
await ctx.send("Must be at least 4 seconds.")
|
await ctx.send(_("Must be at least 4 seconds."))
|
||||||
return
|
return
|
||||||
settings = self.conf.guild(ctx.guild)
|
settings = self.conf.guild(ctx.guild)
|
||||||
await settings.delay.set(seconds)
|
await settings.delay.set(seconds)
|
||||||
await ctx.send("Done. Maximum seconds to answer set to {}.".format(seconds))
|
await ctx.send(_("Done. Maximum seconds to answer set to {num}.").format(num=seconds))
|
||||||
|
|
||||||
@triviaset.command(name="stopafter")
|
@triviaset.command(name="stopafter")
|
||||||
async def triviaset_stopafter(self, ctx: commands.Context, seconds: float):
|
async def triviaset_stopafter(self, ctx: commands.Context, seconds: float):
|
||||||
"""Set how long until trivia stops due to no response."""
|
"""Set how long until trivia stops due to no response."""
|
||||||
settings = self.conf.guild(ctx.guild)
|
settings = self.conf.guild(ctx.guild)
|
||||||
if seconds < await settings.delay():
|
if seconds < await settings.delay():
|
||||||
await ctx.send("Must be larger than the answer time limit.")
|
await ctx.send(_("Must be larger than the answer time limit."))
|
||||||
return
|
return
|
||||||
await settings.timeout.set(seconds)
|
await settings.timeout.set(seconds)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"Done. Trivia sessions will now time out after {}"
|
_(
|
||||||
" seconds of no responses.".format(seconds)
|
"Done. Trivia sessions will now time out after {num} seconds of no responses."
|
||||||
|
).format(num=seconds)
|
||||||
)
|
)
|
||||||
|
|
||||||
@triviaset.command(name="override")
|
@triviaset.command(name="override")
|
||||||
@@ -102,46 +109,46 @@ class Trivia:
|
|||||||
"""Allow/disallow trivia lists to override settings."""
|
"""Allow/disallow trivia lists to override settings."""
|
||||||
settings = self.conf.guild(ctx.guild)
|
settings = self.conf.guild(ctx.guild)
|
||||||
await settings.allow_override.set(enabled)
|
await settings.allow_override.set(enabled)
|
||||||
enabled = "now" if enabled else "no longer"
|
if enabled:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"Done. Trivia lists can {} override the trivia settings"
|
_(
|
||||||
" for this server.".format(enabled)
|
"Done. Trivia lists can now override the trivia settings for this server."
|
||||||
)
|
).format(now=enabled)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await ctx.send(
|
||||||
|
_(
|
||||||
|
"Done. Trivia lists can no longer override the trivia settings for this "
|
||||||
|
"server."
|
||||||
|
).format(now=enabled)
|
||||||
|
)
|
||||||
|
|
||||||
@triviaset.command(name="botplays")
|
@triviaset.command(name="botplays", usage="<true_or_false>")
|
||||||
async def trivaset_bot_plays(self, ctx: commands.Context, true_or_false: bool):
|
async def trivaset_bot_plays(self, ctx: commands.Context, enabled: bool):
|
||||||
"""Set whether or not the bot gains points.
|
"""Set whether or not the bot gains points.
|
||||||
|
|
||||||
If enabled, the bot will gain a point if no one guesses correctly.
|
If enabled, the bot will gain a point if no one guesses correctly.
|
||||||
"""
|
"""
|
||||||
settings = self.conf.guild(ctx.guild)
|
settings = self.conf.guild(ctx.guild)
|
||||||
await settings.bot_plays.set(true_or_false)
|
await settings.bot_plays.set(enabled)
|
||||||
await ctx.send(
|
if enabled:
|
||||||
"Done. "
|
await ctx.send(_("Done. I'll now gain a point if users don't answer in time."))
|
||||||
+ (
|
else:
|
||||||
"I'll gain a point if users don't answer in time."
|
await ctx.send(_("Alright, I won't embarass you at trivia anymore."))
|
||||||
if true_or_false
|
|
||||||
else "Alright, I won't embarass you at trivia anymore."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@triviaset.command(name="revealanswer")
|
@triviaset.command(name="revealanswer", usage="<true_or_false>")
|
||||||
async def trivaset_reveal_answer(self, ctx: commands.Context, true_or_false: bool):
|
async def trivaset_reveal_answer(self, ctx: commands.Context, enabled: bool):
|
||||||
"""Set whether or not the answer is revealed.
|
"""Set whether or not the answer is revealed.
|
||||||
|
|
||||||
If enabled, the bot will reveal the answer if no one guesses correctly
|
If enabled, the bot will reveal the answer if no one guesses correctly
|
||||||
in time.
|
in time.
|
||||||
"""
|
"""
|
||||||
settings = self.conf.guild(ctx.guild)
|
settings = self.conf.guild(ctx.guild)
|
||||||
await settings.reveal_answer.set(true_or_false)
|
await settings.reveal_answer.set(enabled)
|
||||||
await ctx.send(
|
if enabled:
|
||||||
"Done. "
|
await ctx.send(_("Done. I'll reveal the answer if no one knows it."))
|
||||||
+ (
|
else:
|
||||||
"I'll reveal the answer if no one knows it."
|
await ctx.send(_("Alright, I won't reveal the answer to the questions anymore."))
|
||||||
if true_or_false
|
|
||||||
else "I won't reveal the answer to the questions anymore."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@triviaset.command(name="payout")
|
@triviaset.command(name="payout")
|
||||||
@check_global_setting_admin()
|
@check_global_setting_admin()
|
||||||
@@ -157,13 +164,13 @@ class Trivia:
|
|||||||
"""
|
"""
|
||||||
settings = self.conf.guild(ctx.guild)
|
settings = self.conf.guild(ctx.guild)
|
||||||
if multiplier < 0:
|
if multiplier < 0:
|
||||||
await ctx.send("Multiplier must be at least 0.")
|
await ctx.send(_("Multiplier must be at least 0."))
|
||||||
return
|
return
|
||||||
await settings.payout_multiplier.set(multiplier)
|
await settings.payout_multiplier.set(multiplier)
|
||||||
if not multiplier:
|
if multiplier:
|
||||||
await ctx.send("Done. I will no longer reward the winner with a payout.")
|
await ctx.send(_("Done. Payout multiplier set to {num}.").format(num=multiplier))
|
||||||
return
|
else:
|
||||||
await ctx.send("Done. Payout multiplier set to {}.".format(multiplier))
|
await ctx.send(_("Done. I will no longer reward the winner with a payout."))
|
||||||
|
|
||||||
@commands.group(invoke_without_command=True)
|
@commands.group(invoke_without_command=True)
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -179,7 +186,7 @@ class Trivia:
|
|||||||
categories = [c.lower() for c in categories]
|
categories = [c.lower() for c in categories]
|
||||||
session = self._get_trivia_session(ctx.channel)
|
session = self._get_trivia_session(ctx.channel)
|
||||||
if session is not None:
|
if session is not None:
|
||||||
await ctx.send("There is already an ongoing trivia session in this channel.")
|
await ctx.send(_("There is already an ongoing trivia session in this channel."))
|
||||||
return
|
return
|
||||||
trivia_dict = {}
|
trivia_dict = {}
|
||||||
authors = []
|
authors = []
|
||||||
@@ -190,15 +197,17 @@ class Trivia:
|
|||||||
dict_ = self.get_trivia_list(category)
|
dict_ = self.get_trivia_list(category)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"Invalid category `{0}`. See `{1}trivia list`"
|
_(
|
||||||
" for a list of trivia categories."
|
"Invalid category `{name}`. See `{prefix}trivia list` for a list of "
|
||||||
"".format(category, ctx.prefix)
|
"trivia categories."
|
||||||
|
).format(name=category, prefix=ctx.prefix)
|
||||||
)
|
)
|
||||||
except InvalidListError:
|
except InvalidListError:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"There was an error parsing the trivia list for"
|
_(
|
||||||
" the `{}` category. It may be formatted"
|
"There was an error parsing the trivia list for the `{name}` category. It "
|
||||||
" incorrectly.".format(category)
|
"may be formatted incorrectly."
|
||||||
|
).format(name=category)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
trivia_dict.update(dict_)
|
trivia_dict.update(dict_)
|
||||||
@@ -207,7 +216,7 @@ class Trivia:
|
|||||||
return
|
return
|
||||||
if not trivia_dict:
|
if not trivia_dict:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"The trivia list was parsed successfully, however it appears to be empty!"
|
_("The trivia list was parsed successfully, however it appears to be empty!")
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
settings = await self.conf.guild(ctx.guild).all()
|
settings = await self.conf.guild(ctx.guild).all()
|
||||||
@@ -224,7 +233,7 @@ class Trivia:
|
|||||||
"""Stop an ongoing trivia session."""
|
"""Stop an ongoing trivia session."""
|
||||||
session = self._get_trivia_session(ctx.channel)
|
session = self._get_trivia_session(ctx.channel)
|
||||||
if session is None:
|
if session is None:
|
||||||
await ctx.send("There is no ongoing trivia session in this channel.")
|
await ctx.send(_("There is no ongoing trivia session in this channel."))
|
||||||
return
|
return
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
auth_checks = (
|
auth_checks = (
|
||||||
@@ -237,20 +246,28 @@ class Trivia:
|
|||||||
if any(auth_checks):
|
if any(auth_checks):
|
||||||
await session.end_game()
|
await session.end_game()
|
||||||
session.force_stop()
|
session.force_stop()
|
||||||
await ctx.send("Trivia stopped.")
|
await ctx.send(_("Trivia stopped."))
|
||||||
else:
|
else:
|
||||||
await ctx.send("You are not allowed to do that.")
|
await ctx.send(_("You are not allowed to do that."))
|
||||||
|
|
||||||
@trivia.command(name="list")
|
@trivia.command(name="list")
|
||||||
async def trivia_list(self, ctx: commands.Context):
|
async def trivia_list(self, ctx: commands.Context):
|
||||||
"""List available trivia categories."""
|
"""List available trivia categories."""
|
||||||
lists = set(p.stem for p in self._all_lists())
|
lists = set(p.stem for p in self._all_lists())
|
||||||
|
if await ctx.embed_requested():
|
||||||
msg = box("**Available trivia lists**\n\n{}".format(", ".join(sorted(lists))))
|
await ctx.send(
|
||||||
if len(msg) > 1000:
|
embed=discord.Embed(
|
||||||
await ctx.author.send(msg)
|
title=_("Available trivia lists"),
|
||||||
return
|
colour=await ctx.embed_colour(),
|
||||||
await ctx.send(msg)
|
description=", ".join(sorted(lists)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
msg = box(bold(_("Available trivia lists")) + "\n\n" + ", ".join(sorted(lists)))
|
||||||
|
if len(msg) > 1000:
|
||||||
|
await ctx.author.send(msg)
|
||||||
|
else:
|
||||||
|
await ctx.send(msg)
|
||||||
|
|
||||||
@trivia.group(name="leaderboard", aliases=["lboard"], autohelp=False)
|
@trivia.group(name="leaderboard", aliases=["lboard"], autohelp=False)
|
||||||
async def trivia_leaderboard(self, ctx: commands.Context):
|
async def trivia_leaderboard(self, ctx: commands.Context):
|
||||||
@@ -272,19 +289,21 @@ class Trivia:
|
|||||||
):
|
):
|
||||||
"""Leaderboard for this server.
|
"""Leaderboard for this server.
|
||||||
|
|
||||||
<sort_by> can be any of the following fields:
|
`<sort_by>` can be any of the following fields:
|
||||||
- wins : total wins
|
- `wins` : total wins
|
||||||
- avg : average score
|
- `avg` : average score
|
||||||
- total : total correct answers
|
- `total` : total correct answers
|
||||||
|
- `games` : total games played
|
||||||
|
|
||||||
<top> is the number of ranks to show on the leaderboard.
|
`<top>` is the number of ranks to show on the leaderboard.
|
||||||
"""
|
"""
|
||||||
key = self._get_sort_key(sort_by)
|
key = self._get_sort_key(sort_by)
|
||||||
if key is None:
|
if key is None:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"Unknown field `{}`, see `{}help trivia "
|
_(
|
||||||
"leaderboard server` for valid fields to sort by."
|
"Unknown field `{field_name}`, see `{prefix}help trivia leaderboard server` "
|
||||||
"".format(sort_by, ctx.prefix)
|
"for valid fields to sort by."
|
||||||
|
).format(field_name=sort_by, prefix=ctx.prefix)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
@@ -299,20 +318,21 @@ class Trivia:
|
|||||||
):
|
):
|
||||||
"""Global trivia leaderboard.
|
"""Global trivia leaderboard.
|
||||||
|
|
||||||
<sort_by> can be any of the following fields:
|
`<sort_by>` can be any of the following fields:
|
||||||
- wins : total wins
|
- `wins` : total wins
|
||||||
- avg : average score
|
- `avg` : average score
|
||||||
- total : total correct answers from all sessions
|
- `total` : total correct answers from all sessions
|
||||||
- games : total games played
|
- `games` : total games played
|
||||||
|
|
||||||
<top> is the number of ranks to show on the leaderboard.
|
`<top>` is the number of ranks to show on the leaderboard.
|
||||||
"""
|
"""
|
||||||
key = self._get_sort_key(sort_by)
|
key = self._get_sort_key(sort_by)
|
||||||
if key is None:
|
if key is None:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"Unknown field `{}`, see `{}help trivia "
|
_(
|
||||||
"leaderboard global` for valid fields to sort by."
|
"Unknown field `{field_name}`, see `{prefix}help trivia leaderboard server` "
|
||||||
"".format(sort_by, ctx.prefix)
|
"for valid fields to sort by."
|
||||||
|
).format(field_name=sort_by, prefix=ctx.prefix)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
data = await self.conf.all_members()
|
data = await self.conf.all_members()
|
||||||
@@ -364,11 +384,11 @@ class Trivia:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if not data:
|
if not data:
|
||||||
await ctx.send("There are no scores on record!")
|
await ctx.send(_("There are no scores on record!"))
|
||||||
return
|
return
|
||||||
leaderboard = self._get_leaderboard(data, key, top)
|
leaderboard = self._get_leaderboard(data, key, top)
|
||||||
ret = []
|
ret = []
|
||||||
for page in pagify(leaderboard):
|
for page in pagify(leaderboard, shorten_by=10):
|
||||||
ret.append(await ctx.send(box(page, lang="py")))
|
ret.append(await ctx.send(box(page, lang="py")))
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -385,7 +405,7 @@ class Trivia:
|
|||||||
try:
|
try:
|
||||||
priority.remove(key)
|
priority.remove(key)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError("{} is not a valid key.".format(key))
|
raise ValueError(f"{key} is not a valid key.")
|
||||||
# Put key last in reverse priority
|
# Put key last in reverse priority
|
||||||
priority.append(key)
|
priority.append(key)
|
||||||
items = data.items()
|
items = data.items()
|
||||||
@@ -394,16 +414,15 @@ class Trivia:
|
|||||||
max_name_len = max(map(lambda m: len(str(m)), data.keys()))
|
max_name_len = max(map(lambda m: len(str(m)), data.keys()))
|
||||||
# Headers
|
# Headers
|
||||||
headers = (
|
headers = (
|
||||||
"Rank",
|
_("Rank"),
|
||||||
"Member{}".format(" " * (max_name_len - 6)),
|
_("Member") + " " * (max_name_len - 6),
|
||||||
"Wins",
|
_("Wins"),
|
||||||
"Games Played",
|
_("Games Played"),
|
||||||
"Total Score",
|
_("Total Score"),
|
||||||
"Average Score",
|
_("Average Score"),
|
||||||
)
|
)
|
||||||
lines = [" | ".join(headers)]
|
lines = [" | ".join(headers), " | ".join(("-" * len(h) for h in headers))]
|
||||||
# Header underlines
|
# Header underlines
|
||||||
lines.append(" | ".join(("-" * len(h) for h in headers)))
|
|
||||||
for rank, tup in enumerate(items, 1):
|
for rank, tup in enumerate(items, 1):
|
||||||
member, m_data = tup
|
member, m_data = tup
|
||||||
# Align fields to header width
|
# Align fields to header width
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import discord
|
|||||||
|
|
||||||
from redbot.core import Config, checks, commands
|
from redbot.core import Config, checks, commands
|
||||||
from redbot.core.i18n import Translator
|
from redbot.core.i18n import Translator
|
||||||
|
from redbot.core.utils.predicates import MessagePredicate
|
||||||
|
|
||||||
_ = Translator("Warnings", __file__)
|
_ = Translator("Warnings", __file__)
|
||||||
|
|
||||||
@@ -95,11 +96,10 @@ async def get_command_for_exceeded_points(ctx: commands.Context):
|
|||||||
|
|
||||||
await ctx.send(_("You may enter your response now."))
|
await ctx.send(_("You may enter your response now."))
|
||||||
|
|
||||||
def same_author_check(m):
|
|
||||||
return m.author == ctx.author
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
msg = await ctx.bot.wait_for(
|
||||||
|
"message", check=MessagePredicate.same_context(ctx), timeout=30
|
||||||
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
@@ -140,11 +140,10 @@ async def get_command_for_dropping_points(ctx: commands.Context):
|
|||||||
|
|
||||||
await ctx.send(_("You may enter your response now."))
|
await ctx.send(_("You may enter your response now."))
|
||||||
|
|
||||||
def same_author_check(m):
|
|
||||||
return m.author == ctx.author
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
msg = await ctx.bot.wait_for(
|
||||||
|
"message", check=MessagePredicate.same_context(ctx), timeout=30
|
||||||
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -15,19 +15,21 @@ from redbot.core.i18n import Translator, cog_i18n
|
|||||||
from redbot.core.utils.mod import is_admin_or_superior
|
from redbot.core.utils.mod import is_admin_or_superior
|
||||||
from redbot.core.utils.chat_formatting import warning, pagify
|
from redbot.core.utils.chat_formatting import warning, pagify
|
||||||
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||||
|
from redbot.core.utils.predicates import MessagePredicate
|
||||||
|
|
||||||
_ = Translator("Warnings", __file__)
|
_ = Translator("Warnings", __file__)
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class Warnings:
|
class Warnings(commands.Cog):
|
||||||
"""A warning system for Red"""
|
"""Warn misbehaving users and take automated actions."""
|
||||||
|
|
||||||
default_guild = {"actions": [], "reasons": {}, "allow_custom_reasons": False}
|
default_guild = {"actions": [], "reasons": {}, "allow_custom_reasons": False}
|
||||||
|
|
||||||
default_member = {"total_points": 0, "status": "", "warnings": {}}
|
default_member = {"total_points": 0, "status": "", "warnings": {}}
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
|
super().__init__()
|
||||||
self.config = Config.get_conf(self, identifier=5757575755)
|
self.config = Config.get_conf(self, identifier=5757575755)
|
||||||
self.config.register_guild(**self.default_guild)
|
self.config.register_guild(**self.default_guild)
|
||||||
self.config.register_member(**self.default_member)
|
self.config.register_member(**self.default_member)
|
||||||
@@ -46,32 +48,42 @@ class Warnings:
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def warningset(self, ctx: commands.Context):
|
async def warningset(self, ctx: commands.Context):
|
||||||
"""Warning settings"""
|
"""Manage settings for Warnings."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@warningset.command()
|
@warningset.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def allowcustomreasons(self, ctx: commands.Context, allowed: bool):
|
async def allowcustomreasons(self, ctx: commands.Context, allowed: bool):
|
||||||
"""Enable or Disable custom reasons for a warning"""
|
"""Enable or disable custom reasons for a warning."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
await self.config.guild(guild).allow_custom_reasons.set(allowed)
|
await self.config.guild(guild).allow_custom_reasons.set(allowed)
|
||||||
await ctx.send(
|
if allowed:
|
||||||
_("Custom reasons have been {}.").format(_("enabled") if allowed else _("disabled"))
|
await ctx.send(_("Custom reasons have been enabled."))
|
||||||
)
|
else:
|
||||||
|
await ctx.send(_("Custom reasons have been disabled."))
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def warnaction(self, ctx: commands.Context):
|
async def warnaction(self, ctx: commands.Context):
|
||||||
"""Action management"""
|
"""Manage automated actions for Warnings.
|
||||||
|
|
||||||
|
Actions are essentially command macros. Any command can be run
|
||||||
|
when the action is initially triggered, and/or when the action
|
||||||
|
is lifted.
|
||||||
|
|
||||||
|
Actions must be given a name and a points threshold. When a
|
||||||
|
user is warned enough so that their points go over this
|
||||||
|
threshold, the action will be executed.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@warnaction.command(name="add")
|
@warnaction.command(name="add")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def action_add(self, ctx: commands.Context, name: str, points: int):
|
async def action_add(self, ctx: commands.Context, name: str, points: int):
|
||||||
"""Create an action to be taken at a specified point count
|
"""Create an automated action.
|
||||||
|
|
||||||
Duplicate action names are not allowed
|
Duplicate action names are not allowed.
|
||||||
"""
|
"""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
|
||||||
@@ -102,7 +114,7 @@ class Warnings:
|
|||||||
@warnaction.command(name="del")
|
@warnaction.command(name="del")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def action_del(self, ctx: commands.Context, action_name: str):
|
async def action_del(self, ctx: commands.Context, action_name: str):
|
||||||
"""Delete the point count action with the specified name"""
|
"""Delete the action with the specified name."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
guild_settings = self.config.guild(guild)
|
guild_settings = self.config.guild(guild)
|
||||||
async with guild_settings.actions() as registered_actions:
|
async with guild_settings.actions() as registered_actions:
|
||||||
@@ -115,23 +127,29 @@ class Warnings:
|
|||||||
registered_actions.remove(to_remove)
|
registered_actions.remove(to_remove)
|
||||||
await ctx.tick()
|
await ctx.tick()
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("No action named {} exists!").format(action_name))
|
await ctx.send(_("No action named {name} exists!").format(name=action_name))
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(administrator=True)
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
async def warnreason(self, ctx: commands.Context):
|
async def warnreason(self, ctx: commands.Context):
|
||||||
"""Add reasons for warnings"""
|
"""Manage warning reasons.
|
||||||
|
|
||||||
|
Reasons must be given a name, description and points value. The
|
||||||
|
name of the reason must be given when a user is warned.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@warnreason.command(name="add")
|
@warnreason.command(name="create", aliases=["add"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def reason_add(self, ctx: commands.Context, name: str, points: int, *, description: str):
|
async def reason_create(
|
||||||
"""Add a reason to be available for warnings"""
|
self, ctx: commands.Context, name: str, points: int, *, description: str
|
||||||
|
):
|
||||||
|
"""Create a warning reason."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
|
|
||||||
if name.lower() == "custom":
|
if name.lower() == "custom":
|
||||||
await ctx.send("That cannot be used as a reason name!")
|
await ctx.send(_("*Custom* cannot be used as a reason name!"))
|
||||||
return
|
return
|
||||||
to_add = {"points": points, "description": description}
|
to_add = {"points": points, "description": description}
|
||||||
completed = {name.lower(): to_add}
|
completed = {name.lower(): to_add}
|
||||||
@@ -141,12 +159,12 @@ class Warnings:
|
|||||||
async with guild_settings.reasons() as registered_reasons:
|
async with guild_settings.reasons() as registered_reasons:
|
||||||
registered_reasons.update(completed)
|
registered_reasons.update(completed)
|
||||||
|
|
||||||
await ctx.send(_("That reason has been registered."))
|
await ctx.send(_("The new reason has been registered."))
|
||||||
|
|
||||||
@warnreason.command(name="del")
|
@warnreason.command(name="del", aliases=["remove"])
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def reason_del(self, ctx: commands.Context, reason_name: str):
|
async def reason_del(self, ctx: commands.Context, reason_name: str):
|
||||||
"""Delete the reason with the specified name"""
|
"""Delete a warning reason."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
guild_settings = self.config.guild(guild)
|
guild_settings = self.config.guild(guild)
|
||||||
async with guild_settings.reasons() as registered_reasons:
|
async with guild_settings.reasons() as registered_reasons:
|
||||||
@@ -159,7 +177,7 @@ class Warnings:
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.admin_or_permissions(ban_members=True)
|
@checks.admin_or_permissions(ban_members=True)
|
||||||
async def reasonlist(self, ctx: commands.Context):
|
async def reasonlist(self, ctx: commands.Context):
|
||||||
"""List all configured reasons for warnings"""
|
"""List all configured reasons for Warnings."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
guild_settings = self.config.guild(guild)
|
guild_settings = self.config.guild(guild)
|
||||||
msg_list = []
|
msg_list = []
|
||||||
@@ -173,9 +191,9 @@ class Warnings:
|
|||||||
msg_list.append(em)
|
msg_list.append(em)
|
||||||
else:
|
else:
|
||||||
msg_list.append(
|
msg_list.append(
|
||||||
"Name: {}\nPoints: {}\nDescription: {}".format(
|
_(
|
||||||
r, v["points"], v["description"]
|
"Name: {reason_name}\nPoints: {points}\nDescription: {description}"
|
||||||
)
|
).format(reason_name=r, **v)
|
||||||
)
|
)
|
||||||
if msg_list:
|
if msg_list:
|
||||||
await menu(ctx, msg_list, DEFAULT_CONTROLS)
|
await menu(ctx, msg_list, DEFAULT_CONTROLS)
|
||||||
@@ -186,7 +204,7 @@ class Warnings:
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.admin_or_permissions(ban_members=True)
|
@checks.admin_or_permissions(ban_members=True)
|
||||||
async def actionlist(self, ctx: commands.Context):
|
async def actionlist(self, ctx: commands.Context):
|
||||||
"""List the actions to be taken at specific point values"""
|
"""List all configured automated actions for Warnings."""
|
||||||
guild = ctx.guild
|
guild = ctx.guild
|
||||||
guild_settings = self.config.guild(guild)
|
guild_settings = self.config.guild(guild)
|
||||||
msg_list = []
|
msg_list = []
|
||||||
@@ -200,10 +218,10 @@ class Warnings:
|
|||||||
msg_list.append(em)
|
msg_list.append(em)
|
||||||
else:
|
else:
|
||||||
msg_list.append(
|
msg_list.append(
|
||||||
"Name: {}\nPoints: {}\nExceed command: {}\n"
|
_(
|
||||||
"Drop command: {}".format(
|
"Name: {action_name}\nPoints: {points}\n"
|
||||||
r["action_name"], r["points"], r["exceed_command"], r["drop_command"]
|
"Exceed command: {exceed_command}\nDrop command: {drop_command}"
|
||||||
)
|
).format(**r)
|
||||||
)
|
)
|
||||||
if msg_list:
|
if msg_list:
|
||||||
await menu(ctx, msg_list, DEFAULT_CONTROLS)
|
await menu(ctx, msg_list, DEFAULT_CONTROLS)
|
||||||
@@ -214,9 +232,10 @@ class Warnings:
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.admin_or_permissions(ban_members=True)
|
@checks.admin_or_permissions(ban_members=True)
|
||||||
async def warn(self, ctx: commands.Context, user: discord.Member, reason: str):
|
async def warn(self, ctx: commands.Context, user: discord.Member, reason: str):
|
||||||
"""Warn the user for the specified reason
|
"""Warn the user for the specified reason.
|
||||||
|
|
||||||
Reason must be a registered reason, or "custom" if custom reasons are allowed
|
`<reason>` must be a registered reason name, or *custom* if
|
||||||
|
custom reasons are enabled.
|
||||||
"""
|
"""
|
||||||
if user == ctx.author:
|
if user == ctx.author:
|
||||||
await ctx.send(_("You cannot warn yourself."))
|
await ctx.send(_("You cannot warn yourself."))
|
||||||
@@ -226,9 +245,9 @@ class Warnings:
|
|||||||
if not custom_allowed:
|
if not custom_allowed:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"Custom reasons are not allowed! Please see {} for "
|
"Custom reasons are not allowed! Please see `{prefix}reasonlist` for "
|
||||||
"a complete list of valid reasons."
|
"a complete list of valid reasons."
|
||||||
).format("`{}reasonlist`".format(ctx.prefix))
|
).format(prefix=ctx.prefix)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
reason_type = await self.custom_warning_reason(ctx)
|
reason_type = await self.custom_warning_reason(ctx)
|
||||||
@@ -272,9 +291,7 @@ class Warnings:
|
|||||||
await warning_points_add_check(self.config, ctx, user, current_point_count)
|
await warning_points_add_check(self.config, ctx, user, current_point_count)
|
||||||
try:
|
try:
|
||||||
em = discord.Embed(
|
em = discord.Embed(
|
||||||
title=_("Warning from {mod_name}#{mod_discrim}").format(
|
title=_("Warning from {user}").format(user=ctx.author),
|
||||||
mod_name=ctx.author.display_name, mod_discrim=ctx.author.discriminator
|
|
||||||
),
|
|
||||||
description=reason_type["description"],
|
description=reason_type["description"],
|
||||||
)
|
)
|
||||||
em.add_field(name=_("Points"), value=str(reason_type["points"]))
|
em.add_field(name=_("Points"), value=str(reason_type["points"]))
|
||||||
@@ -286,20 +303,17 @@ class Warnings:
|
|||||||
)
|
)
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
pass
|
pass
|
||||||
await ctx.send(
|
await ctx.send(_("User {user} has been warned.").format(user=user))
|
||||||
_("User {user_name}#{user_discrim} has been warned.").format(
|
|
||||||
user_name=user.display_name, user_discrim=user.discriminator
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def warnings(self, ctx: commands.Context, userid: int = None):
|
async def warnings(self, ctx: commands.Context, userid: int = None):
|
||||||
"""Show warnings for the specified user.
|
"""List the warnings for the specified user.
|
||||||
|
|
||||||
|
Emit `<userid>` to see your own warnings.
|
||||||
|
|
||||||
If userid is None, show warnings for the person running the command
|
|
||||||
Note that showing warnings for users other than yourself requires
|
Note that showing warnings for users other than yourself requires
|
||||||
appropriate permissions
|
appropriate permissions.
|
||||||
"""
|
"""
|
||||||
if userid is None:
|
if userid is None:
|
||||||
user = ctx.author
|
user = ctx.author
|
||||||
@@ -327,16 +341,24 @@ class Warnings:
|
|||||||
)
|
)
|
||||||
if mod is None:
|
if mod is None:
|
||||||
mod = await self.bot.get_user_info(user_warnings[key]["mod"])
|
mod = await self.bot.get_user_info(user_warnings[key]["mod"])
|
||||||
msg += "{} point warning {} issued by {} for {}\n".format(
|
msg += _(
|
||||||
user_warnings[key]["points"], key, mod, user_warnings[key]["description"]
|
"{num_points} point warning {reason_name} issued by {user} for "
|
||||||
|
"{description}\n"
|
||||||
|
).format(
|
||||||
|
num_points=user_warnings[key]["points"],
|
||||||
|
reason_name=key,
|
||||||
|
user=mod,
|
||||||
|
description=user_warnings[key]["description"],
|
||||||
)
|
)
|
||||||
await ctx.send_interactive(pagify(msg), box_lang="Warnings for {}".format(user))
|
await ctx.send_interactive(
|
||||||
|
pagify(msg, shorten_by=58), box_lang=_("Warnings for {user}").format(user=user)
|
||||||
|
)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.admin_or_permissions(ban_members=True)
|
@checks.admin_or_permissions(ban_members=True)
|
||||||
async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str):
|
async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str):
|
||||||
"""Removes the specified warning from the user specified"""
|
"""Remove a warning from a user."""
|
||||||
if user_id == ctx.author.id:
|
if user_id == ctx.author.id:
|
||||||
await ctx.send(_("You cannot remove warnings from yourself."))
|
await ctx.send(_("You cannot remove warnings from yourself."))
|
||||||
return
|
return
|
||||||
@@ -350,7 +372,7 @@ class Warnings:
|
|||||||
await warning_points_remove_check(self.config, ctx, member, current_point_count)
|
await warning_points_remove_check(self.config, ctx, member, current_point_count)
|
||||||
async with member_settings.warnings() as user_warnings:
|
async with member_settings.warnings() as user_warnings:
|
||||||
if warn_id not in user_warnings.keys():
|
if warn_id not in user_warnings.keys():
|
||||||
await ctx.send("That warning doesn't exist!")
|
await ctx.send(_("That warning doesn't exist!"))
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
current_point_count -= user_warnings[warn_id]["points"]
|
current_point_count -= user_warnings[warn_id]["points"]
|
||||||
@@ -363,12 +385,11 @@ class Warnings:
|
|||||||
"""Handles getting description and points for custom reasons"""
|
"""Handles getting description and points for custom reasons"""
|
||||||
to_add = {"points": 0, "description": ""}
|
to_add = {"points": 0, "description": ""}
|
||||||
|
|
||||||
def same_author_check(m):
|
|
||||||
return m.author == ctx.author
|
|
||||||
|
|
||||||
await ctx.send(_("How many points should be given for this reason?"))
|
await ctx.send(_("How many points should be given for this reason?"))
|
||||||
try:
|
try:
|
||||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
msg = await ctx.bot.wait_for(
|
||||||
|
"message", check=MessagePredicate.same_context(ctx), timeout=30
|
||||||
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send(_("Ok then."))
|
await ctx.send(_("Ok then."))
|
||||||
return
|
return
|
||||||
@@ -385,7 +406,9 @@ class Warnings:
|
|||||||
|
|
||||||
await ctx.send(_("Enter a description for this reason."))
|
await ctx.send(_("Enter a description for this reason."))
|
||||||
try:
|
try:
|
||||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
|
msg = await ctx.bot.wait_for(
|
||||||
|
"message", check=MessagePredicate.same_context(ctx), timeout=30
|
||||||
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send(_("Ok then."))
|
await ctx.send(_("Ok then."))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ __all__ = ["Config", "__version__"]
|
|||||||
|
|
||||||
class VersionInfo:
|
class VersionInfo:
|
||||||
def __init__(self, major, minor, micro, releaselevel, serial):
|
def __init__(self, major, minor, micro, releaselevel, serial):
|
||||||
self._levels = ["alpha", "beta", "final"]
|
self._levels = ["alpha", "beta", "release candidate", "final"]
|
||||||
self.major = major
|
self.major = major
|
||||||
self.minor = minor
|
self.minor = minor
|
||||||
self.micro = micro
|
self.micro = micro
|
||||||
@@ -36,5 +36,5 @@ class VersionInfo:
|
|||||||
return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
|
return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
|
||||||
|
|
||||||
|
|
||||||
__version__ = "3.0.0b21"
|
__version__ = "3.0.0rc1"
|
||||||
version_info = VersionInfo(3, 0, 0, "beta", 21)
|
version_info = VersionInfo(3, 0, 0, "release candidate", 1)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
from typing import Union, List
|
from typing import Union, List, Optional
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
@@ -296,12 +296,20 @@ async def transfer_credits(from_: discord.Member, to: discord.Member, amount: in
|
|||||||
return await deposit_credits(to, amount)
|
return await deposit_credits(to, amount)
|
||||||
|
|
||||||
|
|
||||||
async def wipe_bank():
|
async def wipe_bank(guild: Optional[discord.Guild] = None) -> None:
|
||||||
"""Delete all accounts from the bank."""
|
"""Delete all accounts from the bank.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
guild : discord.Guild
|
||||||
|
The guild to clear accounts for. If unsupplied and the bank is
|
||||||
|
per-server, all accounts in every guild will be wiped.
|
||||||
|
|
||||||
|
"""
|
||||||
if await is_global():
|
if await is_global():
|
||||||
await _conf.clear_all_users()
|
await _conf.clear_all_users()
|
||||||
else:
|
else:
|
||||||
await _conf.clear_all_members()
|
await _conf.clear_all_members(guild)
|
||||||
|
|
||||||
|
|
||||||
async def get_leaderboard(positions: int = None, guild: discord.Guild = None) -> List[tuple]:
|
async def get_leaderboard(positions: int = None, guild: discord.Guild = None) -> List[tuple]:
|
||||||
|
|||||||
@@ -5,16 +5,12 @@ from collections import Counter
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from importlib.machinery import ModuleSpec
|
from importlib.machinery import ModuleSpec
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional, Union, List
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
import sys
|
import sys
|
||||||
from discord.ext.commands import when_mentioned_or
|
from discord.ext.commands import when_mentioned_or
|
||||||
|
|
||||||
# This supresses the PyNaCl warning that isn't relevant here
|
|
||||||
from discord.voice_client import VoiceClient
|
|
||||||
|
|
||||||
VoiceClient.warn_nacl = False
|
|
||||||
|
|
||||||
from .cog_manager import CogManager
|
from .cog_manager import CogManager
|
||||||
from . import Config, i18n, commands
|
from . import Config, i18n, commands
|
||||||
from .rpc import RPCMixin
|
from .rpc import RPCMixin
|
||||||
@@ -72,6 +68,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
use_bot_color=False,
|
use_bot_color=False,
|
||||||
fuzzy=False,
|
fuzzy=False,
|
||||||
disabled_commands=[],
|
disabled_commands=[],
|
||||||
|
autoimmune_ids=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.db.register_user(embeds=None)
|
self.db.register_user(embeds=None)
|
||||||
@@ -122,6 +119,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
self.add_command(help_)
|
self.add_command(help_)
|
||||||
|
|
||||||
self._sentry_mgr = None
|
self._sentry_mgr = None
|
||||||
|
self._permissions_hooks: List[commands.CheckPredicate] = []
|
||||||
|
|
||||||
def enable_sentry(self):
|
def enable_sentry(self):
|
||||||
"""Enable Sentry logging for Red."""
|
"""Enable Sentry logging for Red."""
|
||||||
@@ -198,7 +196,8 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
async def get_context(self, message, *, cls=commands.Context):
|
async def get_context(self, message, *, cls=commands.Context):
|
||||||
return await super().get_context(message, cls=cls)
|
return await super().get_context(message, cls=cls)
|
||||||
|
|
||||||
def list_packages(self):
|
@staticmethod
|
||||||
|
def list_packages():
|
||||||
"""Lists packages present in the cogs the folder"""
|
"""Lists packages present in the cogs the folder"""
|
||||||
return os.listdir("cogs")
|
return os.listdir("cogs")
|
||||||
|
|
||||||
@@ -232,7 +231,26 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
|
|
||||||
self.extensions[name] = lib
|
self.extensions[name] = lib
|
||||||
|
|
||||||
def remove_cog(self, cogname):
|
def remove_cog(self, cogname: str):
|
||||||
|
cog = self.get_cog(cogname)
|
||||||
|
if cog is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
for when in ("before", "after"):
|
||||||
|
try:
|
||||||
|
hook = getattr(cog, f"_{cog.__class__.__name__}__red_permissions_{when}")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.remove_permissions_hook(hook, when)
|
||||||
|
|
||||||
|
try:
|
||||||
|
hook = getattr(cog, f"_{cog.__class__.__name__}__red_permissions_before")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.remove_permissions_hook(hook)
|
||||||
|
|
||||||
super().remove_cog(cogname)
|
super().remove_cog(cogname)
|
||||||
|
|
||||||
for meth in self.rpc_handlers.pop(cogname.upper(), ()):
|
for meth in self.rpc_handlers.pop(cogname.upper(), ()):
|
||||||
@@ -294,6 +312,42 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
if pkg_name.startswith("redbot.cogs."):
|
if pkg_name.startswith("redbot.cogs."):
|
||||||
del sys.modules["redbot.cogs"].__dict__[name]
|
del sys.modules["redbot.cogs"].__dict__[name]
|
||||||
|
|
||||||
|
async def is_automod_immune(
|
||||||
|
self, to_check: Union[discord.Message, commands.Context, discord.abc.User, discord.Role]
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if the user, message, context, or role should be considered immune from automated
|
||||||
|
moderation actions.
|
||||||
|
|
||||||
|
This will return ``False`` in direct messages.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
to_check : `discord.Message` or `commands.Context` or `discord.abc.User` or `discord.Role`
|
||||||
|
Something to check if it would be immune
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
``True`` if immune
|
||||||
|
|
||||||
|
"""
|
||||||
|
guild = to_check.guild
|
||||||
|
if not guild:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if isinstance(to_check, discord.Role):
|
||||||
|
ids_to_check = [to_check.id]
|
||||||
|
else:
|
||||||
|
author = getattr(to_check, "author", to_check)
|
||||||
|
ids_to_check = [r.id for r in author.roles]
|
||||||
|
ids_to_check.append(author.id)
|
||||||
|
|
||||||
|
immune_ids = await self.db.guild(guild).autoimmune_ids()
|
||||||
|
|
||||||
|
return any(i in immune_ids for i in ids_to_check)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
async def send_filtered(
|
async def send_filtered(
|
||||||
destination: discord.abc.Messageable,
|
destination: discord.abc.Messageable,
|
||||||
filter_mass_mentions=True,
|
filter_mass_mentions=True,
|
||||||
@@ -327,9 +381,19 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
|
|
||||||
await destination.send(content=content, **kwargs)
|
await destination.send(content=content, **kwargs)
|
||||||
|
|
||||||
def add_cog(self, cog):
|
def add_cog(self, cog: commands.Cog):
|
||||||
|
if not isinstance(cog, commands.Cog):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"The {cog.__class__.__name__} cog in the {cog.__module__} package does "
|
||||||
|
f"not inherit from the commands.Cog base class. The cog author must update "
|
||||||
|
f"the cog to adhere to this requirement."
|
||||||
|
)
|
||||||
|
if not hasattr(cog, "requires"):
|
||||||
|
commands.Cog.__init__(cog)
|
||||||
for attr in dir(cog):
|
for attr in dir(cog):
|
||||||
_attr = getattr(cog, attr)
|
_attr = getattr(cog, attr)
|
||||||
|
if attr == f"_{cog.__class__.__name__}__permissions_hook":
|
||||||
|
self.add_permissions_hook(_attr)
|
||||||
if isinstance(_attr, discord.ext.commands.Command) and not isinstance(
|
if isinstance(_attr, discord.ext.commands.Command) and not isinstance(
|
||||||
_attr, commands.Command
|
_attr, commands.Command
|
||||||
):
|
):
|
||||||
@@ -342,6 +406,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
"http://red-discordbot.readthedocs.io/en/v3-develop/framework_commands.html"
|
"http://red-discordbot.readthedocs.io/en/v3-develop/framework_commands.html"
|
||||||
)
|
)
|
||||||
super().add_cog(cog)
|
super().add_cog(cog)
|
||||||
|
self.dispatch("cog_add", cog)
|
||||||
|
|
||||||
def add_command(self, command: commands.Command):
|
def add_command(self, command: commands.Command):
|
||||||
if not isinstance(command, commands.Command):
|
if not isinstance(command, commands.Command):
|
||||||
@@ -350,6 +415,76 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
super().add_command(command)
|
super().add_command(command)
|
||||||
self.dispatch("command_add", command)
|
self.dispatch("command_add", command)
|
||||||
|
|
||||||
|
def clear_permission_rules(self, guild_id: Optional[int]) -> None:
|
||||||
|
"""Clear all permission overrides in a scope.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
guild_id : Optional[int]
|
||||||
|
The guild ID to wipe permission overrides for. If
|
||||||
|
``None``, this will clear all global rules and leave all
|
||||||
|
guild rules untouched.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for cog in self.cogs.values():
|
||||||
|
cog.requires.clear_all_rules(guild_id)
|
||||||
|
for command in self.walk_commands():
|
||||||
|
command.requires.clear_all_rules(guild_id)
|
||||||
|
|
||||||
|
def add_permissions_hook(self, hook: commands.CheckPredicate) -> None:
|
||||||
|
"""Add a permissions hook.
|
||||||
|
|
||||||
|
Permissions hooks are check predicates which are called before
|
||||||
|
calling `Requires.verify`, and they can optionally return an
|
||||||
|
override: ``True`` to allow, ``False`` to deny, and ``None`` to
|
||||||
|
default to normal behaviour.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
hook
|
||||||
|
A command check predicate which returns ``True``, ``False``
|
||||||
|
or ``None``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._permissions_hooks.append(hook)
|
||||||
|
|
||||||
|
def remove_permissions_hook(self, hook: commands.CheckPredicate) -> None:
|
||||||
|
"""Remove a permissions hook.
|
||||||
|
|
||||||
|
Parameters are the same as those in `add_permissions_hook`.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If the permissions hook has not been added.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._permissions_hooks.remove(hook)
|
||||||
|
|
||||||
|
async def verify_permissions_hooks(self, ctx: commands.Context) -> Optional[bool]:
|
||||||
|
"""Run permissions hooks.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ctx : commands.Context
|
||||||
|
The context for the command being invoked.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Optional[bool]
|
||||||
|
``False`` if any hooks returned ``False``, ``True`` if any
|
||||||
|
hooks return ``True`` and none returned ``False``, ``None``
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
hook_results = []
|
||||||
|
for hook in self._permissions_hooks:
|
||||||
|
result = await discord.utils.maybe_coroutine(hook, ctx)
|
||||||
|
if result is not None:
|
||||||
|
hook_results.append(result)
|
||||||
|
if hook_results:
|
||||||
|
return all(hook_results)
|
||||||
|
|
||||||
|
|
||||||
class Red(RedBase, discord.AutoShardedClient):
|
class Red(RedBase, discord.AutoShardedClient):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,126 +1,77 @@
|
|||||||
|
import warnings
|
||||||
|
from typing import Awaitable, TYPE_CHECKING, Dict
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import commands
|
|
||||||
|
from .commands import (
|
||||||
|
bot_has_permissions,
|
||||||
|
has_permissions,
|
||||||
|
is_owner,
|
||||||
|
guildowner,
|
||||||
|
guildowner_or_permissions,
|
||||||
|
admin,
|
||||||
|
admin_or_permissions,
|
||||||
|
mod,
|
||||||
|
mod_or_permissions,
|
||||||
|
check as _check_decorator,
|
||||||
|
)
|
||||||
|
from .utils.mod import (
|
||||||
|
is_mod_or_superior as _is_mod_or_superior,
|
||||||
|
is_admin_or_superior as _is_admin_or_superior,
|
||||||
|
check_permissions as _check_permissions,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .bot import Red
|
||||||
|
from .commands import Context
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"bot_has_permissions",
|
||||||
|
"has_permissions",
|
||||||
|
"is_owner",
|
||||||
|
"guildowner",
|
||||||
|
"guildowner_or_permissions",
|
||||||
|
"admin",
|
||||||
|
"admin_or_permissions",
|
||||||
|
"mod",
|
||||||
|
"mod_or_permissions",
|
||||||
|
"is_mod_or_superior",
|
||||||
|
"is_admin_or_superior",
|
||||||
|
"bot_in_a_guild",
|
||||||
|
"check_permissions",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def check_overrides(ctx, *, level):
|
def bot_in_a_guild():
|
||||||
if await ctx.bot.is_owner(ctx.author):
|
"""Deny the command if the bot is not in a guild."""
|
||||||
return True
|
|
||||||
perm_cog = ctx.bot.get_cog("Permissions")
|
|
||||||
if not perm_cog or ctx.cog == perm_cog:
|
|
||||||
return None
|
|
||||||
# don't break if someone loaded a cog named
|
|
||||||
# permissions that doesn't implement this
|
|
||||||
func = getattr(perm_cog, "check_overrides", None)
|
|
||||||
val = None if func is None else await func(ctx, level)
|
|
||||||
return val
|
|
||||||
|
|
||||||
|
|
||||||
def is_owner(**kwargs):
|
|
||||||
async def check(ctx):
|
|
||||||
return await ctx.bot.is_owner(ctx.author, **kwargs)
|
|
||||||
|
|
||||||
return commands.check(check)
|
|
||||||
|
|
||||||
|
|
||||||
async def check_permissions(ctx, perms):
|
|
||||||
if await ctx.bot.is_owner(ctx.author):
|
|
||||||
return True
|
|
||||||
elif not perms:
|
|
||||||
return False
|
|
||||||
resolved = ctx.channel.permissions_for(ctx.author)
|
|
||||||
|
|
||||||
return resolved.administrator or all(
|
|
||||||
getattr(resolved, name, None) == value for name, value in perms.items()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def is_mod_or_superior(ctx):
|
|
||||||
if ctx.guild is None:
|
|
||||||
return await ctx.bot.is_owner(ctx.author)
|
|
||||||
else:
|
|
||||||
author = ctx.author
|
|
||||||
settings = ctx.bot.db.guild(ctx.guild)
|
|
||||||
mod_role_id = await settings.mod_role()
|
|
||||||
admin_role_id = await settings.admin_role()
|
|
||||||
|
|
||||||
mod_role = discord.utils.get(ctx.guild.roles, id=mod_role_id)
|
|
||||||
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id)
|
|
||||||
|
|
||||||
return (
|
|
||||||
await ctx.bot.is_owner(ctx.author)
|
|
||||||
or mod_role in author.roles
|
|
||||||
or admin_role in author.roles
|
|
||||||
or author == ctx.guild.owner
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def is_admin_or_superior(ctx):
|
|
||||||
if ctx.guild is None:
|
|
||||||
return await ctx.bot.is_owner(ctx.author)
|
|
||||||
else:
|
|
||||||
author = ctx.author
|
|
||||||
settings = ctx.bot.db.guild(ctx.guild)
|
|
||||||
admin_role_id = await settings.admin_role()
|
|
||||||
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id)
|
|
||||||
|
|
||||||
return (
|
|
||||||
await ctx.bot.is_owner(ctx.author)
|
|
||||||
or admin_role in author.roles
|
|
||||||
or author == ctx.guild.owner
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def mod_or_permissions(**perms):
|
|
||||||
async def predicate(ctx):
|
|
||||||
override = await check_overrides(ctx, level="mod")
|
|
||||||
return (
|
|
||||||
override
|
|
||||||
if override is not None
|
|
||||||
else await check_permissions(ctx, perms) or await is_mod_or_superior(ctx)
|
|
||||||
)
|
|
||||||
|
|
||||||
return commands.check(predicate)
|
|
||||||
|
|
||||||
|
|
||||||
def admin_or_permissions(**perms):
|
|
||||||
async def predicate(ctx):
|
|
||||||
override = await check_overrides(ctx, level="admin")
|
|
||||||
return (
|
|
||||||
override
|
|
||||||
if override is not None
|
|
||||||
else await check_permissions(ctx, perms) or await is_admin_or_superior(ctx)
|
|
||||||
)
|
|
||||||
|
|
||||||
return commands.check(predicate)
|
|
||||||
|
|
||||||
|
|
||||||
def bot_in_a_guild(**kwargs):
|
|
||||||
async def predicate(ctx):
|
async def predicate(ctx):
|
||||||
return len(ctx.bot.guilds) > 0
|
return len(ctx.bot.guilds) > 0
|
||||||
|
|
||||||
return commands.check(predicate)
|
return _check_decorator(predicate)
|
||||||
|
|
||||||
|
|
||||||
def guildowner_or_permissions(**perms):
|
def is_mod_or_superior(ctx: "Context") -> Awaitable[bool]:
|
||||||
async def predicate(ctx):
|
warnings.warn(
|
||||||
has_perms_or_is_owner = await check_permissions(ctx, perms)
|
"`redbot.core.checks.is_mod_or_superior` is deprecated and will be removed in a future "
|
||||||
if ctx.guild is None:
|
"release, please use `redbot.core.utils.mod.is_mod_or_superior` instead.",
|
||||||
return has_perms_or_is_owner
|
category=DeprecationWarning,
|
||||||
is_guild_owner = ctx.author == ctx.guild.owner
|
)
|
||||||
|
return _is_mod_or_superior(ctx.bot, ctx.author)
|
||||||
override = await check_overrides(ctx, level="guildowner")
|
|
||||||
return override if override is not None else is_guild_owner or has_perms_or_is_owner
|
|
||||||
|
|
||||||
return commands.check(predicate)
|
|
||||||
|
|
||||||
|
|
||||||
def guildowner():
|
def is_admin_or_superior(ctx: "Context") -> Awaitable[bool]:
|
||||||
return guildowner_or_permissions()
|
warnings.warn(
|
||||||
|
"`redbot.core.checks.is_admin_or_superior` is deprecated and will be removed in a future "
|
||||||
|
"release, please use `redbot.core.utils.mod.is_admin_or_superior` instead.",
|
||||||
|
category=DeprecationWarning,
|
||||||
|
)
|
||||||
|
return _is_admin_or_superior(ctx.bot, ctx.author)
|
||||||
|
|
||||||
|
|
||||||
def admin():
|
def check_permissions(ctx: "Context", perms: Dict[str, bool]) -> Awaitable[bool]:
|
||||||
return admin_or_permissions()
|
warnings.warn(
|
||||||
|
"`redbot.core.checks.check_permissions` is deprecated and will be removed in a future "
|
||||||
|
"release, please use `redbot.core.utils.mod.check_permissions`."
|
||||||
def mod():
|
)
|
||||||
return mod_or_permissions()
|
return _check_permissions(ctx, perms)
|
||||||
|
|||||||
@@ -50,12 +50,10 @@ def interactive_config(red, token_set, prefix_set):
|
|||||||
def ask_sentry(red: Red):
|
def ask_sentry(red: Red):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
print(
|
print(
|
||||||
"\nThank you for installing Red V3 beta! The current version\n"
|
"\nThank you for installing Red V3! Red is constantly undergoing\n"
|
||||||
" is not suited for production use and is aimed at testing\n"
|
" improvements, and we would like ask if you are comfortable with\n"
|
||||||
" the current and upcoming featureset, that's why we will\n"
|
" the bot automatically submitting fatal error logs to the development\n"
|
||||||
" also collect the fatal error logs to help us fix any new\n"
|
' team. If you wish to opt into the process please type "yes":\n'
|
||||||
" found issues in a timely manner. If you wish to opt in\n"
|
|
||||||
' the process please type "yes":\n'
|
|
||||||
)
|
)
|
||||||
if not confirm("> "):
|
if not confirm("> "):
|
||||||
loop.run_until_complete(red.db.enable_sentry.set(False))
|
loop.run_until_complete(red.db.enable_sentry.set(False))
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ _ = Translator("CogManagerUI", __file__)
|
|||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
class CogManagerUI:
|
class CogManagerUI(commands.Cog):
|
||||||
"""Commands to interface with Red's cog manager."""
|
"""Commands to interface with Red's cog manager."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
from discord.ext.commands import *
|
from discord.ext.commands import *
|
||||||
from .commands import *
|
from .commands import *
|
||||||
from .context import *
|
from .context import *
|
||||||
|
from .converter import *
|
||||||
from .errors import *
|
from .errors import *
|
||||||
|
from .requires import *
|
||||||
|
|||||||
@@ -5,33 +5,118 @@ replace those from the `discord.ext.commands` module.
|
|||||||
"""
|
"""
|
||||||
import inspect
|
import inspect
|
||||||
import weakref
|
import weakref
|
||||||
from typing import Awaitable, Callable, TYPE_CHECKING
|
from typing import Awaitable, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from . import converter as converters
|
||||||
from .errors import ConversionFailure
|
from .errors import ConversionFailure
|
||||||
|
from .requires import PermState, PrivilegeLevel, Requires
|
||||||
from ..i18n import Translator
|
from ..i18n import Translator
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .context import Context
|
from .context import Context
|
||||||
|
|
||||||
__all__ = ["Command", "GroupMixin", "Group", "command", "group"]
|
__all__ = [
|
||||||
|
"Cog",
|
||||||
|
"CogCommandMixin",
|
||||||
|
"CogGroupMixin",
|
||||||
|
"Command",
|
||||||
|
"Group",
|
||||||
|
"GroupMixin",
|
||||||
|
"command",
|
||||||
|
"group",
|
||||||
|
]
|
||||||
|
|
||||||
_ = Translator("commands.commands", __file__)
|
_ = Translator("commands.commands", __file__)
|
||||||
|
|
||||||
|
|
||||||
class Command(commands.Command):
|
class CogCommandMixin:
|
||||||
|
"""A mixin for cogs and commands."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if isinstance(self, Command):
|
||||||
|
decorated = self.callback
|
||||||
|
else:
|
||||||
|
decorated = self
|
||||||
|
self.requires: Requires = Requires(
|
||||||
|
privilege_level=getattr(
|
||||||
|
decorated, "__requires_privilege_level__", PrivilegeLevel.NONE
|
||||||
|
),
|
||||||
|
user_perms=getattr(decorated, "__requires_user_perms__", {}),
|
||||||
|
bot_perms=getattr(decorated, "__requires_bot_perms__", {}),
|
||||||
|
checks=getattr(decorated, "__requires_checks__", []),
|
||||||
|
)
|
||||||
|
|
||||||
|
def allow_for(self, model_id: int, guild_id: int) -> None:
|
||||||
|
"""Actively allow this command for the given model."""
|
||||||
|
self.requires.set_rule(model_id, PermState.ACTIVE_ALLOW, guild_id=guild_id)
|
||||||
|
|
||||||
|
def deny_to(self, model_id: int, guild_id: int) -> None:
|
||||||
|
"""Actively deny this command to the given model."""
|
||||||
|
cur_rule = self.requires.get_rule(model_id, guild_id=guild_id)
|
||||||
|
if cur_rule is PermState.PASSIVE_ALLOW:
|
||||||
|
self.requires.set_rule(model_id, PermState.CAUTIOUS_ALLOW, guild_id=guild_id)
|
||||||
|
else:
|
||||||
|
self.requires.set_rule(model_id, PermState.ACTIVE_DENY, guild_id=guild_id)
|
||||||
|
|
||||||
|
def clear_rule_for(self, model_id: int, guild_id: int) -> Tuple[PermState, PermState]:
|
||||||
|
"""Clear the rule which is currently set for this model."""
|
||||||
|
cur_rule = self.requires.get_rule(model_id, guild_id=guild_id)
|
||||||
|
if cur_rule is PermState.ACTIVE_ALLOW:
|
||||||
|
new_rule = PermState.NORMAL
|
||||||
|
elif cur_rule is PermState.ACTIVE_DENY:
|
||||||
|
new_rule = PermState.NORMAL
|
||||||
|
elif cur_rule is PermState.CAUTIOUS_ALLOW:
|
||||||
|
new_rule = PermState.PASSIVE_ALLOW
|
||||||
|
else:
|
||||||
|
return cur_rule, cur_rule
|
||||||
|
self.requires.set_rule(model_id, new_rule, guild_id=guild_id)
|
||||||
|
return cur_rule, new_rule
|
||||||
|
|
||||||
|
def set_default_rule(self, rule: Optional[bool], guild_id: int) -> None:
|
||||||
|
"""Set the default rule for this cog or command.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
rule : Optional[bool]
|
||||||
|
The rule to set as default. If ``True`` for allow,
|
||||||
|
``False`` for deny and ``None`` for normal.
|
||||||
|
guild_id : Optional[int]
|
||||||
|
Specify to set the default rule for a specific guild.
|
||||||
|
When ``None``, this will set the global default rule.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if guild_id:
|
||||||
|
self.requires.set_default_guild_rule(guild_id, PermState.from_bool(rule))
|
||||||
|
else:
|
||||||
|
self.requires.default_global_rule = PermState.from_bool(rule)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(CogCommandMixin, commands.Command):
|
||||||
"""Command class for Red.
|
"""Command class for Red.
|
||||||
|
|
||||||
This should not be created directly, and instead via the decorator.
|
This should not be created directly, and instead via the decorator.
|
||||||
|
|
||||||
This class inherits from `discord.ext.commands.Command`.
|
This class inherits from `discord.ext.commands.Command`. The
|
||||||
|
attributes listed below are simply additions to the ones listed
|
||||||
|
with that class.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
checks : List[`coroutine function`]
|
||||||
|
A list of check predicates which cannot be overridden, unlike
|
||||||
|
`Requires.checks`.
|
||||||
|
translator : Translator
|
||||||
|
A translator for this command's help docstring.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self._help_override = kwargs.pop("help_override", None)
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self._help_override = kwargs.pop("help_override", None)
|
||||||
self.translator = kwargs.pop("i18n", None)
|
self.translator = kwargs.pop("i18n", None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -59,11 +144,10 @@ class Command(commands.Command):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parents(self):
|
def parents(self) -> List["Group"]:
|
||||||
"""
|
"""List[Group] : Returns all parent commands of this command.
|
||||||
Returns all parent commands of this command.
|
|
||||||
|
|
||||||
This is a list, sorted by the length of :attr:`.qualified_name` from highest to lowest.
|
This is sorted by the length of :attr:`.qualified_name` from highest to lowest.
|
||||||
If the command has no parents, this will be an empty list.
|
If the command has no parents, this will be an empty list.
|
||||||
"""
|
"""
|
||||||
cmd = self.parent
|
cmd = self.parent
|
||||||
@@ -73,6 +157,33 @@ class Command(commands.Command):
|
|||||||
cmd = cmd.parent
|
cmd = cmd.parent
|
||||||
return sorted(entries, key=lambda x: len(x.qualified_name), reverse=True)
|
return sorted(entries, key=lambda x: len(x.qualified_name), reverse=True)
|
||||||
|
|
||||||
|
async def can_run(self, ctx: "Context") -> bool:
|
||||||
|
"""Check if this command can be run in the given context.
|
||||||
|
|
||||||
|
This function first checks if the command can be run using
|
||||||
|
discord.py's method `discord.ext.commands.Command.can_run`,
|
||||||
|
then will return the result of `Requires.verify`.
|
||||||
|
"""
|
||||||
|
ret = await super().can_run(ctx)
|
||||||
|
if ret is False:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# This is so contexts invoking other commands can be checked with
|
||||||
|
# this command as well
|
||||||
|
original_command = ctx.command
|
||||||
|
ctx.command = self
|
||||||
|
|
||||||
|
if self.parent is None and self.instance is not None:
|
||||||
|
# For top-level commands, we need to check the cog's requires too
|
||||||
|
ret = await self.instance.requires.verify(ctx)
|
||||||
|
if ret is False:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await self.requires.verify(ctx)
|
||||||
|
finally:
|
||||||
|
ctx.command = original_command
|
||||||
|
|
||||||
async def do_conversion(
|
async def do_conversion(
|
||||||
self, ctx: "Context", converter, argument: str, param: inspect.Parameter
|
self, ctx: "Context", converter, argument: str, param: inspect.Parameter
|
||||||
):
|
):
|
||||||
@@ -106,6 +217,36 @@ class Command(commands.Command):
|
|||||||
# We should expose anything which might be a bug in the converter
|
# We should expose anything which might be a bug in the converter
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
|
async def can_see(self, ctx: "Context"):
|
||||||
|
"""Check if this command is visible in the given context.
|
||||||
|
|
||||||
|
In short, this will verify whether the user can run the
|
||||||
|
command, and also whether the command is hidden or not.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ctx : `Context`
|
||||||
|
The invocation context to check with.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
``True`` if this command is visible in the given context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for cmd in (self, *self.parents):
|
||||||
|
if cmd.hidden:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
can_run = await self.can_run(ctx)
|
||||||
|
except commands.CheckFailure:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if can_run is False:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def disable_in(self, guild: discord.Guild) -> bool:
|
def disable_in(self, guild: discord.Guild) -> bool:
|
||||||
"""Disable this command in the given guild.
|
"""Disable this command in the given guild.
|
||||||
|
|
||||||
@@ -149,8 +290,32 @@ class Command(commands.Command):
|
|||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def allow_for(self, model_id: int, guild_id: int) -> None:
|
||||||
|
super().allow_for(model_id, guild_id=guild_id)
|
||||||
|
parents = self.parents
|
||||||
|
if self.instance is not None:
|
||||||
|
parents.append(self.instance)
|
||||||
|
for parent in parents:
|
||||||
|
cur_rule = parent.requires.get_rule(model_id, guild_id=guild_id)
|
||||||
|
if cur_rule is PermState.NORMAL:
|
||||||
|
parent.requires.set_rule(model_id, PermState.PASSIVE_ALLOW, guild_id=guild_id)
|
||||||
|
elif cur_rule is PermState.ACTIVE_DENY:
|
||||||
|
parent.requires.set_rule(model_id, PermState.CAUTIOUS_ALLOW, guild_id=guild_id)
|
||||||
|
|
||||||
class GroupMixin(commands.GroupMixin):
|
def clear_rule_for(self, model_id: int, guild_id: int) -> Tuple[PermState, PermState]:
|
||||||
|
old_rule, new_rule = super().clear_rule_for(model_id, guild_id=guild_id)
|
||||||
|
if old_rule is PermState.ACTIVE_ALLOW:
|
||||||
|
parents = self.parents
|
||||||
|
if self.instance is not None:
|
||||||
|
parents.append(self.instance)
|
||||||
|
for parent in parents:
|
||||||
|
should_continue = parent.reevaluate_rules_for(model_id, guild_id=guild_id)[1]
|
||||||
|
if not should_continue:
|
||||||
|
break
|
||||||
|
return old_rule, new_rule
|
||||||
|
|
||||||
|
|
||||||
|
class GroupMixin(discord.ext.commands.GroupMixin):
|
||||||
"""Mixin for `Group` and `Red` classes.
|
"""Mixin for `Group` and `Red` classes.
|
||||||
|
|
||||||
This class inherits from :class:`discord.ext.commands.GroupMixin`.
|
This class inherits from :class:`discord.ext.commands.GroupMixin`.
|
||||||
@@ -181,7 +346,34 @@ class GroupMixin(commands.GroupMixin):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
class Group(GroupMixin, Command, commands.Group):
|
class CogGroupMixin:
|
||||||
|
requires: Requires
|
||||||
|
all_commands: Dict[str, Command]
|
||||||
|
|
||||||
|
def reevaluate_rules_for(
|
||||||
|
self, model_id: int, guild_id: Optional[int]
|
||||||
|
) -> Tuple[PermState, bool]:
|
||||||
|
cur_rule = self.requires.get_rule(model_id, guild_id=guild_id)
|
||||||
|
if cur_rule in (PermState.NORMAL, PermState.ACTIVE_ALLOW, PermState.ACTIVE_DENY):
|
||||||
|
# These three states are unaffected by subcommand rules
|
||||||
|
return cur_rule, False
|
||||||
|
else:
|
||||||
|
# Remaining states can be changed if there exists no actively-allowed
|
||||||
|
# subcommand (this includes subcommands multiple levels below)
|
||||||
|
if any(
|
||||||
|
cmd.requires.get_rule(model_id, guild_id=guild_id) in PermState.ALLOWED_STATES
|
||||||
|
for cmd in self.all_commands.values()
|
||||||
|
):
|
||||||
|
return cur_rule, False
|
||||||
|
elif cur_rule is PermState.PASSIVE_ALLOW:
|
||||||
|
self.requires.set_rule(model_id, PermState.NORMAL, guild_id=guild_id)
|
||||||
|
return PermState.NORMAL, True
|
||||||
|
elif cur_rule is PermState.CAUTIOUS_ALLOW:
|
||||||
|
self.requires.set_rule(model_id, PermState.ACTIVE_DENY, guild_id=guild_id)
|
||||||
|
return PermState.ACTIVE_DENY, True
|
||||||
|
|
||||||
|
|
||||||
|
class Group(GroupMixin, Command, CogGroupMixin, commands.Group):
|
||||||
"""Group command class for Red.
|
"""Group command class for Red.
|
||||||
|
|
||||||
This class inherits from `Command`, with :class:`GroupMixin` and
|
This class inherits from `Command`, with :class:`GroupMixin` and
|
||||||
@@ -192,7 +384,7 @@ class Group(GroupMixin, Command, commands.Group):
|
|||||||
self.autohelp = kwargs.pop("autohelp", True)
|
self.autohelp = kwargs.pop("autohelp", True)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
async def invoke(self, ctx):
|
async def invoke(self, ctx: "Context"):
|
||||||
view = ctx.view
|
view = ctx.view
|
||||||
previous = view.index
|
previous = view.index
|
||||||
view.skip_ws()
|
view.skip_ws()
|
||||||
@@ -217,7 +409,12 @@ class Group(GroupMixin, Command, commands.Group):
|
|||||||
await super().invoke(ctx)
|
await super().invoke(ctx)
|
||||||
|
|
||||||
|
|
||||||
# decorators
|
class Cog(CogCommandMixin, CogGroupMixin):
|
||||||
|
"""Base class for a cog."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_commands(self) -> Dict[str, Command]:
|
||||||
|
return {cmd.name: cmd for cmd in self.__dict__.values() if isinstance(cmd, Command)}
|
||||||
|
|
||||||
|
|
||||||
def command(name=None, cls=Command, **attrs):
|
def command(name=None, cls=Command, **attrs):
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Iterable, List
|
from typing import Iterable, List
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
from redbot.core.utils.chat_formatting import box
|
from .requires import PermState
|
||||||
from redbot.core.utils import common_filters
|
from ..utils.chat_formatting import box
|
||||||
|
from ..utils.predicates import MessagePredicate
|
||||||
|
from ..utils import common_filters
|
||||||
|
|
||||||
TICK = "\N{WHITE HEAVY CHECK MARK}"
|
TICK = "\N{WHITE HEAVY CHECK MARK}"
|
||||||
|
|
||||||
@@ -20,6 +21,10 @@ class Context(commands.Context):
|
|||||||
This class inherits from `discord.ext.commands.Context`.
|
This class inherits from `discord.ext.commands.Context`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **attrs):
|
||||||
|
super().__init__(**attrs)
|
||||||
|
self.permission_state: PermState = PermState.NORMAL
|
||||||
|
|
||||||
async def send(self, content=None, **kwargs):
|
async def send(self, content=None, **kwargs):
|
||||||
"""Sends a message to the destination with the content given.
|
"""Sends a message to the destination with the content given.
|
||||||
|
|
||||||
@@ -136,10 +141,6 @@ class Context(commands.Context):
|
|||||||
messages = tuple(messages)
|
messages = tuple(messages)
|
||||||
ret = []
|
ret = []
|
||||||
|
|
||||||
more_check = lambda m: (
|
|
||||||
m.author == self.author and m.channel == self.channel and m.content.lower() == "more"
|
|
||||||
)
|
|
||||||
|
|
||||||
for idx, page in enumerate(messages, 1):
|
for idx, page in enumerate(messages, 1):
|
||||||
if box_lang is None:
|
if box_lang is None:
|
||||||
msg = await self.send(page)
|
msg = await self.send(page)
|
||||||
@@ -160,7 +161,11 @@ class Context(commands.Context):
|
|||||||
"".format(is_are, n_remaining, plural)
|
"".format(is_are, n_remaining, plural)
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
resp = await self.bot.wait_for("message", check=more_check, timeout=timeout)
|
resp = await self.bot.wait_for(
|
||||||
|
"message",
|
||||||
|
check=MessagePredicate.lower_equal_to("more", self),
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await query.delete()
|
await query.delete()
|
||||||
break
|
break
|
||||||
@@ -170,7 +175,7 @@ class Context(commands.Context):
|
|||||||
except (discord.HTTPException, AttributeError):
|
except (discord.HTTPException, AttributeError):
|
||||||
# In case the bot can't delete other users' messages,
|
# In case the bot can't delete other users' messages,
|
||||||
# or is not a bot account
|
# or is not a bot account
|
||||||
# or chanel is a DM
|
# or channel is a DM
|
||||||
await query.delete()
|
await query.delete()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -237,3 +242,20 @@ class Context(commands.Context):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return await self.send(message)
|
return await self.send(message)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def clean_prefix(self) -> str:
|
||||||
|
"""str: The command prefix, but a mention prefix is displayed nicer."""
|
||||||
|
me = self.me
|
||||||
|
return self.prefix.replace(me.mention, f"@{me.display_name}")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def me(self) -> discord.abc.User:
|
||||||
|
"""discord.abc.User: The bot member or user object.
|
||||||
|
|
||||||
|
If the context is DM, this will be a `discord.User` object.
|
||||||
|
"""
|
||||||
|
if self.guild is not None:
|
||||||
|
return self.guild.me
|
||||||
|
else:
|
||||||
|
return self.bot.user
|
||||||
|
|||||||
41
redbot/core/commands/converter.py
Normal file
41
redbot/core/commands/converter.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import re
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
from . import BadArgument
|
||||||
|
from ..i18n import Translator
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .context import Context
|
||||||
|
|
||||||
|
__all__ = ["GuildConverter"]
|
||||||
|
|
||||||
|
_ = Translator("commands.converter", __file__)
|
||||||
|
|
||||||
|
ID_REGEX = re.compile(r"([0-9]{15,21})")
|
||||||
|
|
||||||
|
|
||||||
|
class GuildConverter(discord.Guild):
|
||||||
|
"""Converts to a `discord.Guild` object.
|
||||||
|
|
||||||
|
The lookup strategy is as follows (in order):
|
||||||
|
|
||||||
|
1. Lookup by ID.
|
||||||
|
2. Lookup by name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def convert(cls, ctx: "Context", argument: str) -> discord.Guild:
|
||||||
|
match = ID_REGEX.fullmatch(argument)
|
||||||
|
|
||||||
|
if match is None:
|
||||||
|
ret = discord.utils.get(ctx.bot.guilds, name=argument)
|
||||||
|
else:
|
||||||
|
guild_id = int(match.group(1))
|
||||||
|
ret = ctx.bot.get_guild(guild_id)
|
||||||
|
|
||||||
|
if ret is None:
|
||||||
|
raise BadArgument(_('Server "{name}" not found.').format(name=argument))
|
||||||
|
|
||||||
|
return ret
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
"""Errors module for the commands package."""
|
"""Errors module for the commands package."""
|
||||||
import inspect
|
import inspect
|
||||||
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
__all__ = ["ConversionFailure"]
|
__all__ = ["ConversionFailure", "BotMissingPermissions"]
|
||||||
|
|
||||||
|
|
||||||
class ConversionFailure(commands.BadArgument):
|
class ConversionFailure(commands.BadArgument):
|
||||||
@@ -13,3 +14,11 @@ class ConversionFailure(commands.BadArgument):
|
|||||||
self.argument = argument
|
self.argument = argument
|
||||||
self.param = param
|
self.param = param
|
||||||
super().__init__(*args)
|
super().__init__(*args)
|
||||||
|
|
||||||
|
|
||||||
|
class BotMissingPermissions(commands.CheckFailure):
|
||||||
|
"""Raised if the bot is missing permissions required to run a command."""
|
||||||
|
|
||||||
|
def __init__(self, missing: discord.Permissions, *args):
|
||||||
|
self.missing: discord.Permissions = missing
|
||||||
|
super().__init__(*args)
|
||||||
|
|||||||
668
redbot/core/commands/requires.py
Normal file
668
redbot/core/commands/requires.py
Normal file
@@ -0,0 +1,668 @@
|
|||||||
|
"""
|
||||||
|
commands.requires
|
||||||
|
=================
|
||||||
|
This module manages the logic of resolving command permissions and
|
||||||
|
requirements. This includes rules which override those requirements,
|
||||||
|
as well as custom checks which can be overriden, and some special
|
||||||
|
checks like bot permissions checks.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import enum
|
||||||
|
from typing import (
|
||||||
|
Union,
|
||||||
|
Optional,
|
||||||
|
List,
|
||||||
|
Callable,
|
||||||
|
Awaitable,
|
||||||
|
Dict,
|
||||||
|
Any,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
TypeVar,
|
||||||
|
Tuple,
|
||||||
|
)
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
from .converter import GuildConverter
|
||||||
|
from .errors import BotMissingPermissions
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .commands import Command
|
||||||
|
from .context import Context
|
||||||
|
|
||||||
|
_CommandOrCoro = TypeVar("_CommandOrCoro", Callable[..., Awaitable[Any]], Command)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"CheckPredicate",
|
||||||
|
"DM_PERMS",
|
||||||
|
"GlobalPermissionModel",
|
||||||
|
"GuildPermissionModel",
|
||||||
|
"PermissionModel",
|
||||||
|
"PrivilegeLevel",
|
||||||
|
"PermState",
|
||||||
|
"Requires",
|
||||||
|
"permissions_check",
|
||||||
|
"bot_has_permissions",
|
||||||
|
"has_permissions",
|
||||||
|
"is_owner",
|
||||||
|
"guildowner",
|
||||||
|
"guildowner_or_permissions",
|
||||||
|
"admin",
|
||||||
|
"admin_or_permissions",
|
||||||
|
"mod",
|
||||||
|
"mod_or_permissions",
|
||||||
|
]
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
GlobalPermissionModel = Union[
|
||||||
|
discord.User,
|
||||||
|
discord.VoiceChannel,
|
||||||
|
discord.TextChannel,
|
||||||
|
discord.CategoryChannel,
|
||||||
|
discord.Role,
|
||||||
|
GuildConverter, # Unfortunately this will have to do for now
|
||||||
|
]
|
||||||
|
GuildPermissionModel = Union[
|
||||||
|
discord.Member,
|
||||||
|
discord.VoiceChannel,
|
||||||
|
discord.TextChannel,
|
||||||
|
discord.CategoryChannel,
|
||||||
|
discord.Role,
|
||||||
|
GuildConverter,
|
||||||
|
]
|
||||||
|
PermissionModel = Union[GlobalPermissionModel, GuildPermissionModel]
|
||||||
|
CheckPredicate = Callable[["Context"], Union[Optional[bool], Awaitable[Optional[bool]]]]
|
||||||
|
|
||||||
|
# Here we are trying to model DM permissions as closely as possible. The only
|
||||||
|
# discrepancy I've found is that users can pin messages, but they cannot delete them.
|
||||||
|
# This means manage_messages is only half True, so it's left as False.
|
||||||
|
# This is also the same as the permissions returned when `permissions_for` is used in DM.
|
||||||
|
DM_PERMS = discord.Permissions.none()
|
||||||
|
DM_PERMS.update(
|
||||||
|
add_reactions=True,
|
||||||
|
attach_files=True,
|
||||||
|
embed_links=True,
|
||||||
|
external_emojis=True,
|
||||||
|
mention_everyone=True,
|
||||||
|
read_message_history=True,
|
||||||
|
read_messages=True,
|
||||||
|
send_messages=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PrivilegeLevel(enum.IntEnum):
|
||||||
|
"""Enumeration for special privileges."""
|
||||||
|
|
||||||
|
NONE = enum.auto()
|
||||||
|
"""No special privilege level."""
|
||||||
|
|
||||||
|
MOD = enum.auto()
|
||||||
|
"""User has the mod role."""
|
||||||
|
|
||||||
|
ADMIN = enum.auto()
|
||||||
|
"""User has the admin role."""
|
||||||
|
|
||||||
|
GUILD_OWNER = enum.auto()
|
||||||
|
"""User is the guild level."""
|
||||||
|
|
||||||
|
BOT_OWNER = enum.auto()
|
||||||
|
"""User is a bot owner."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def from_ctx(cls, ctx: "Context") -> "PrivilegeLevel":
|
||||||
|
"""Get a command author's PrivilegeLevel based on context."""
|
||||||
|
if await ctx.bot.is_owner(ctx.author):
|
||||||
|
return cls.BOT_OWNER
|
||||||
|
elif ctx.guild is None:
|
||||||
|
return cls.NONE
|
||||||
|
elif ctx.author == ctx.guild.owner:
|
||||||
|
return cls.GUILD_OWNER
|
||||||
|
|
||||||
|
# The following is simply an optimised way to check if the user has the
|
||||||
|
# admin or mod role.
|
||||||
|
guild_settings = ctx.bot.db.guild(ctx.guild)
|
||||||
|
admin_role_id = await guild_settings.admin_role()
|
||||||
|
mod_role_id = await guild_settings.mod_role()
|
||||||
|
is_mod = False
|
||||||
|
for role in ctx.author.roles:
|
||||||
|
if role.id == admin_role_id:
|
||||||
|
return cls.ADMIN
|
||||||
|
elif role.id == mod_role_id:
|
||||||
|
is_mod = True
|
||||||
|
if is_mod:
|
||||||
|
return cls.MOD
|
||||||
|
|
||||||
|
return cls.NONE
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{self.__class__.__name__}.{self.name}>"
|
||||||
|
|
||||||
|
|
||||||
|
class PermState(enum.Enum):
|
||||||
|
"""Enumeration for permission states used by rules."""
|
||||||
|
|
||||||
|
ACTIVE_ALLOW = enum.auto()
|
||||||
|
"""This command has been actively allowed, default user checks
|
||||||
|
should be ignored.
|
||||||
|
"""
|
||||||
|
|
||||||
|
NORMAL = enum.auto()
|
||||||
|
"""No overrides have been set for this command, make determination
|
||||||
|
from default user checks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PASSIVE_ALLOW = enum.auto()
|
||||||
|
"""There exists a subcommand in the `ACTIVE_ALLOW` state, continue
|
||||||
|
down the subcommand tree until we either find it or realise we're
|
||||||
|
on the wrong branch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CAUTIOUS_ALLOW = enum.auto()
|
||||||
|
"""This command has been actively denied, but there exists a
|
||||||
|
subcommand in the `ACTIVE_ALLOW` state. This occurs when
|
||||||
|
`PASSIVE_ALLOW` and `ACTIVE_DENY` are combined.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ACTIVE_DENY = enum.auto()
|
||||||
|
"""This command has been actively denied, terminate the command
|
||||||
|
chain.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def transition_to(
|
||||||
|
self, next_state: "PermState"
|
||||||
|
) -> Tuple[Optional[bool], Union["PermState", Dict[bool, "PermState"]]]:
|
||||||
|
return self.TRANSITIONS[self][next_state]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bool(cls, value: Optional[bool]) -> "PermState":
|
||||||
|
"""Get a PermState from a bool or ``NoneType``."""
|
||||||
|
if value is True:
|
||||||
|
return cls.ACTIVE_ALLOW
|
||||||
|
elif value is False:
|
||||||
|
return cls.ACTIVE_DENY
|
||||||
|
else:
|
||||||
|
return cls.NORMAL
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{self.__class__.__name__}.{self.name}>"
|
||||||
|
|
||||||
|
|
||||||
|
# Here we're defining how we transition between states.
|
||||||
|
# The dict is in the form:
|
||||||
|
# previous state -> this state -> Tuple[override, next state]
|
||||||
|
# "override" is a bool describing whether or not the command should be
|
||||||
|
# invoked. It can be None, in which case the default permission checks
|
||||||
|
# will be used instead.
|
||||||
|
# There is also one case where the "next state" is dependent on the
|
||||||
|
# result of the default permission checks - the transition from NORMAL
|
||||||
|
# to PASSIVE_ALLOW. In this case "next state" is a dict mapping the
|
||||||
|
# permission check results to the actual next state.
|
||||||
|
PermState.TRANSITIONS = {
|
||||||
|
PermState.ACTIVE_ALLOW: {
|
||||||
|
PermState.ACTIVE_ALLOW: (True, PermState.ACTIVE_ALLOW),
|
||||||
|
PermState.NORMAL: (True, PermState.ACTIVE_ALLOW),
|
||||||
|
PermState.PASSIVE_ALLOW: (True, PermState.ACTIVE_ALLOW),
|
||||||
|
PermState.CAUTIOUS_ALLOW: (True, PermState.CAUTIOUS_ALLOW),
|
||||||
|
PermState.ACTIVE_DENY: (False, PermState.ACTIVE_DENY),
|
||||||
|
},
|
||||||
|
PermState.NORMAL: {
|
||||||
|
PermState.ACTIVE_ALLOW: (True, PermState.ACTIVE_ALLOW),
|
||||||
|
PermState.NORMAL: (None, PermState.NORMAL),
|
||||||
|
PermState.PASSIVE_ALLOW: (True, {True: PermState.NORMAL, False: PermState.PASSIVE_ALLOW}),
|
||||||
|
PermState.CAUTIOUS_ALLOW: (True, PermState.CAUTIOUS_ALLOW),
|
||||||
|
PermState.ACTIVE_DENY: (False, PermState.ACTIVE_DENY),
|
||||||
|
},
|
||||||
|
PermState.PASSIVE_ALLOW: {
|
||||||
|
PermState.ACTIVE_ALLOW: (True, PermState.ACTIVE_ALLOW),
|
||||||
|
PermState.NORMAL: (False, PermState.NORMAL),
|
||||||
|
PermState.PASSIVE_ALLOW: (True, PermState.PASSIVE_ALLOW),
|
||||||
|
PermState.CAUTIOUS_ALLOW: (True, PermState.CAUTIOUS_ALLOW),
|
||||||
|
PermState.ACTIVE_DENY: (False, PermState.ACTIVE_DENY),
|
||||||
|
},
|
||||||
|
PermState.CAUTIOUS_ALLOW: {
|
||||||
|
PermState.ACTIVE_ALLOW: (True, PermState.ACTIVE_ALLOW),
|
||||||
|
PermState.NORMAL: (False, PermState.ACTIVE_DENY),
|
||||||
|
PermState.PASSIVE_ALLOW: (True, PermState.CAUTIOUS_ALLOW),
|
||||||
|
PermState.CAUTIOUS_ALLOW: (True, PermState.CAUTIOUS_ALLOW),
|
||||||
|
PermState.ACTIVE_DENY: (False, PermState.ACTIVE_DENY),
|
||||||
|
},
|
||||||
|
PermState.ACTIVE_DENY: { # We can only start from ACTIVE_DENY if it is set on a cog.
|
||||||
|
PermState.ACTIVE_ALLOW: (True, PermState.ACTIVE_ALLOW), # Should never happen
|
||||||
|
PermState.NORMAL: (False, PermState.ACTIVE_DENY),
|
||||||
|
PermState.PASSIVE_ALLOW: (False, PermState.ACTIVE_DENY), # Should never happen
|
||||||
|
PermState.CAUTIOUS_ALLOW: (False, PermState.ACTIVE_DENY), # Should never happen
|
||||||
|
PermState.ACTIVE_DENY: (False, PermState.ACTIVE_DENY),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
PermState.ALLOWED_STATES = (
|
||||||
|
PermState.ACTIVE_ALLOW,
|
||||||
|
PermState.PASSIVE_ALLOW,
|
||||||
|
PermState.CAUTIOUS_ALLOW,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Requires:
|
||||||
|
"""This class describes the requirements for executing a specific command.
|
||||||
|
|
||||||
|
The permissions described include both bot permissions and user
|
||||||
|
permissions.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
checks : List[Callable[[Context], Union[bool, Awaitable[bool]]]]
|
||||||
|
A list of checks which can be overridden by rules. Use
|
||||||
|
`Command.checks` if you would like them to never be overridden.
|
||||||
|
privilege_level : PrivilegeLevel
|
||||||
|
The required privilege level (bot owner, admin, etc.) for users
|
||||||
|
to execute the command. Can be ``None``, in which case the
|
||||||
|
`user_perms` will be used exclusively, otherwise, for levels
|
||||||
|
other than bot owner, the user can still run the command if
|
||||||
|
they have the required `user_perms`.
|
||||||
|
user_perms : Optional[discord.Permissions]
|
||||||
|
The required permissions for users to execute the command. Can
|
||||||
|
be ``None``, in which case the `privilege_level` will be used
|
||||||
|
exclusively, otherwise, it will pass whether the user has the
|
||||||
|
required `privilege_level` _or_ `user_perms`.
|
||||||
|
bot_perms : discord.Permissions
|
||||||
|
The required bot permissions for a command to be executed. This
|
||||||
|
is not overrideable by other conditions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
privilege_level: Optional[PrivilegeLevel],
|
||||||
|
user_perms: Union[Dict[str, bool], discord.Permissions, None],
|
||||||
|
bot_perms: Union[Dict[str, bool], discord.Permissions],
|
||||||
|
checks: List[CheckPredicate],
|
||||||
|
):
|
||||||
|
self.checks: List[CheckPredicate] = checks
|
||||||
|
self.privilege_level: Optional[PrivilegeLevel] = privilege_level
|
||||||
|
|
||||||
|
if isinstance(user_perms, dict):
|
||||||
|
self.user_perms: Optional[discord.Permissions] = discord.Permissions.none()
|
||||||
|
self.user_perms.update(**user_perms)
|
||||||
|
else:
|
||||||
|
self.user_perms = user_perms
|
||||||
|
|
||||||
|
if isinstance(bot_perms, dict):
|
||||||
|
self.bot_perms: discord.Permissions = discord.Permissions.none()
|
||||||
|
self.bot_perms.update(**bot_perms)
|
||||||
|
else:
|
||||||
|
self.bot_perms = bot_perms
|
||||||
|
self.default_global_rule: PermState = PermState.NORMAL
|
||||||
|
self._global_rules: _IntKeyDict[PermState] = _IntKeyDict()
|
||||||
|
self._default_guild_rules: _IntKeyDict[PermState] = _IntKeyDict()
|
||||||
|
self._guild_rules: _IntKeyDict[_IntKeyDict[PermState]] = _IntKeyDict()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_decorator(
|
||||||
|
privilege_level: Optional[PrivilegeLevel], user_perms: Dict[str, bool]
|
||||||
|
) -> Callable[["_CommandOrCoro"], "_CommandOrCoro"]:
|
||||||
|
if not user_perms:
|
||||||
|
user_perms = None
|
||||||
|
|
||||||
|
def decorator(func: "_CommandOrCoro") -> "_CommandOrCoro":
|
||||||
|
if asyncio.iscoroutinefunction(func):
|
||||||
|
func.__requires_privilege_level__ = privilege_level
|
||||||
|
func.__requires_user_perms__ = user_perms
|
||||||
|
else:
|
||||||
|
func.requires.privilege_level = privilege_level
|
||||||
|
if user_perms is None:
|
||||||
|
func.requires.user_perms = None
|
||||||
|
else:
|
||||||
|
func.requires.user_perms.update(**user_perms)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def get_rule(self, model: Union[int, PermissionModel], guild_id: int) -> PermState:
|
||||||
|
"""Get the rule for a particular model.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
model : PermissionModel
|
||||||
|
The model to get the rule for.
|
||||||
|
guild_id : int
|
||||||
|
The ID of the guild for the rule's scope. Set to ``0``
|
||||||
|
for a global rule.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
PermState
|
||||||
|
The state for this rule. See the `PermState` class
|
||||||
|
for an explanation.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not isinstance(model, int):
|
||||||
|
model = model.id
|
||||||
|
if guild_id:
|
||||||
|
rules = self._guild_rules.get(guild_id, _IntKeyDict())
|
||||||
|
else:
|
||||||
|
rules = self._global_rules
|
||||||
|
return rules.get(model, PermState.NORMAL)
|
||||||
|
|
||||||
|
def set_rule(self, model_id: int, rule: PermState, guild_id: int) -> None:
|
||||||
|
"""Set the rule for a particular model.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
model_id : PermissionModel
|
||||||
|
The model to add a rule for.
|
||||||
|
rule : PermState
|
||||||
|
Which state this rule should be set as. See the `PermState`
|
||||||
|
class for an explanation.
|
||||||
|
guild_id : int
|
||||||
|
The ID of the guild for the rule's scope. Set to ``0``
|
||||||
|
for a global rule.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if guild_id:
|
||||||
|
rules = self._guild_rules.setdefault(guild_id, _IntKeyDict())
|
||||||
|
else:
|
||||||
|
rules = self._global_rules
|
||||||
|
if rule is PermState.NORMAL:
|
||||||
|
rules.pop(model_id, None)
|
||||||
|
else:
|
||||||
|
rules[model_id] = rule
|
||||||
|
|
||||||
|
def clear_all_rules(self, guild_id: int) -> None:
|
||||||
|
"""Clear all rules of a particular scope.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
guild_id : int
|
||||||
|
The guild ID to clear rules for. If ``0``, this will
|
||||||
|
clear all global rules and leave all guild rules
|
||||||
|
untouched.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if guild_id:
|
||||||
|
rules = self._guild_rules.setdefault(guild_id, _IntKeyDict())
|
||||||
|
else:
|
||||||
|
rules = self._global_rules
|
||||||
|
rules.clear()
|
||||||
|
|
||||||
|
def get_default_guild_rule(self, guild_id: int) -> PermState:
|
||||||
|
"""Get the default rule for a guild."""
|
||||||
|
return self._default_guild_rules.get(guild_id, PermState.NORMAL)
|
||||||
|
|
||||||
|
def set_default_guild_rule(self, guild_id: int, rule: PermState) -> None:
|
||||||
|
"""Set the default rule for a guild."""
|
||||||
|
self._default_guild_rules[guild_id] = rule
|
||||||
|
|
||||||
|
async def verify(self, ctx: "Context") -> bool:
|
||||||
|
"""Check if the given context passes the requirements.
|
||||||
|
|
||||||
|
This will check the bot permissions, overrides, user permissions
|
||||||
|
and privilege level.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ctx : "Context"
|
||||||
|
The invkokation context to check with.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
``True`` if the context passes the requirements.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
BotMissingPermissions
|
||||||
|
If the bot is missing required permissions to run the
|
||||||
|
command.
|
||||||
|
CommandError
|
||||||
|
Propogated from any permissions checks.
|
||||||
|
|
||||||
|
"""
|
||||||
|
await self._verify_bot(ctx)
|
||||||
|
# Owner-only commands are non-overrideable
|
||||||
|
if self.privilege_level is PrivilegeLevel.BOT_OWNER:
|
||||||
|
return await ctx.bot.is_owner(ctx.author)
|
||||||
|
|
||||||
|
hook_result = await ctx.bot.verify_permissions_hooks(ctx)
|
||||||
|
if hook_result is not None:
|
||||||
|
return hook_result
|
||||||
|
|
||||||
|
return await self._transition_state(ctx)
|
||||||
|
|
||||||
|
async def _verify_bot(self, ctx: "Context") -> None:
|
||||||
|
if ctx.guild is None:
|
||||||
|
bot_user = ctx.bot.user
|
||||||
|
else:
|
||||||
|
bot_user = ctx.guild.me
|
||||||
|
bot_perms = ctx.channel.permissions_for(bot_user)
|
||||||
|
if not (bot_perms.administrator or bot_perms >= self.bot_perms):
|
||||||
|
raise BotMissingPermissions(missing=self._missing_perms(self.bot_perms, bot_perms))
|
||||||
|
|
||||||
|
async def _transition_state(self, ctx: "Context") -> bool:
|
||||||
|
prev_state = ctx.permission_state
|
||||||
|
cur_state = self._get_rule_from_ctx(ctx)
|
||||||
|
should_invoke, next_state = prev_state.transition_to(cur_state)
|
||||||
|
if should_invoke is None:
|
||||||
|
# NORMAL invokation, we simply follow standard procedure
|
||||||
|
should_invoke = await self._verify_user(ctx)
|
||||||
|
elif isinstance(next_state, dict):
|
||||||
|
# NORMAL to PASSIVE_ALLOW; should we proceed as normal or transition?
|
||||||
|
next_state = next_state[await self._verify_user(ctx)]
|
||||||
|
|
||||||
|
ctx.permission_state = next_state
|
||||||
|
return should_invoke
|
||||||
|
|
||||||
|
async def _verify_user(self, ctx: "Context") -> bool:
|
||||||
|
checks_pass = await self._verify_checks(ctx)
|
||||||
|
if checks_pass is False:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.user_perms is not None:
|
||||||
|
user_perms = ctx.channel.permissions_for(ctx.author)
|
||||||
|
if user_perms.administrator or user_perms >= self.user_perms:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self.privilege_level is not None:
|
||||||
|
privilege_level = await PrivilegeLevel.from_ctx(ctx)
|
||||||
|
if privilege_level >= self.privilege_level:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_rule_from_ctx(self, ctx: "Context") -> PermState:
|
||||||
|
author = ctx.author
|
||||||
|
guild = ctx.guild
|
||||||
|
if ctx.guild is None:
|
||||||
|
# We only check the user for DM channels
|
||||||
|
rule = self._global_rules.get(author.id)
|
||||||
|
if rule is not None:
|
||||||
|
return rule
|
||||||
|
return self.default_global_rule
|
||||||
|
|
||||||
|
rules_chain = [self._global_rules]
|
||||||
|
guild_rules = self._guild_rules.get(ctx.guild.id)
|
||||||
|
if guild_rules:
|
||||||
|
rules_chain.append(guild_rules)
|
||||||
|
|
||||||
|
channels = []
|
||||||
|
if author.voice is not None:
|
||||||
|
channels.append(author.voice.channel)
|
||||||
|
channels.append(ctx.channel)
|
||||||
|
category = ctx.channel.category
|
||||||
|
if category is not None:
|
||||||
|
channels.append(category)
|
||||||
|
|
||||||
|
model_chain = [author, *channels, *author.roles, guild]
|
||||||
|
|
||||||
|
for rules in rules_chain:
|
||||||
|
for model in model_chain:
|
||||||
|
rule = rules.get(model.id)
|
||||||
|
if rule is not None:
|
||||||
|
return rule
|
||||||
|
del model_chain[-1] # We don't check for the guild in guild rules
|
||||||
|
|
||||||
|
default_rule = self.get_default_guild_rule(guild.id)
|
||||||
|
if default_rule is PermState.NORMAL:
|
||||||
|
default_rule = self.default_global_rule
|
||||||
|
return default_rule
|
||||||
|
|
||||||
|
async def _verify_checks(self, ctx: "Context") -> bool:
|
||||||
|
if not self.checks:
|
||||||
|
return True
|
||||||
|
return await discord.utils.async_all(check(ctx) for check in self.checks)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_perms_for(ctx: "Context", user: discord.abc.User) -> discord.Permissions:
|
||||||
|
if ctx.guild is None:
|
||||||
|
return DM_PERMS
|
||||||
|
else:
|
||||||
|
return ctx.channel.permissions_for(user)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_bot_perms(cls, ctx: "Context") -> discord.Permissions:
|
||||||
|
return cls._get_perms_for(ctx, ctx.guild.me if ctx.guild else ctx.bot.user)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _missing_perms(
|
||||||
|
required: discord.Permissions, actual: discord.Permissions
|
||||||
|
) -> discord.Permissions:
|
||||||
|
# Explained in set theory terms:
|
||||||
|
# Assuming R is the set of required permissions, and A is
|
||||||
|
# the set of the user's permissions, the set of missing
|
||||||
|
# permissions will be equal to R \ A, i.e. the relative
|
||||||
|
# complement/difference of A with respect to R.
|
||||||
|
relative_complement = required.value & ~actual.value
|
||||||
|
return discord.Permissions(relative_complement)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _member_as_user(member: discord.abc.User) -> discord.User:
|
||||||
|
if isinstance(member, discord.Member):
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
return member._user
|
||||||
|
return member
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Requires privilege_level={self.privilege_level!r} user_perms={self.user_perms!r} "
|
||||||
|
f"bot_perms={self.bot_perms!r}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# check decorators
|
||||||
|
|
||||||
|
|
||||||
|
def permissions_check(predicate: CheckPredicate):
|
||||||
|
"""An overwriteable version of `discord.ext.commands.check`.
|
||||||
|
|
||||||
|
This has the same behaviour as `discord.ext.commands.check`,
|
||||||
|
however this check can be ignored if the command is allowed
|
||||||
|
through a permissions cog.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func: "_CommandOrCoro") -> "_CommandOrCoro":
|
||||||
|
if hasattr(func, "requires"):
|
||||||
|
func.requires.checks.append(predicate)
|
||||||
|
else:
|
||||||
|
if not hasattr(func, "__requires_checks__"):
|
||||||
|
func.__requires_checks__ = []
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
func.__requires_checks__.append(predicate)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def bot_has_permissions(**perms: bool):
|
||||||
|
"""Complain if the bot is missing permissions.
|
||||||
|
|
||||||
|
If the user tries to run the command, but the bot is missing the
|
||||||
|
permissions, it will send a message describing which permissions
|
||||||
|
are missing.
|
||||||
|
|
||||||
|
This check cannot be overridden by rules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func: "_CommandOrCoro") -> "_CommandOrCoro":
|
||||||
|
if asyncio.iscoroutinefunction(func):
|
||||||
|
func.__requires_bot_perms__ = perms
|
||||||
|
else:
|
||||||
|
func.requires.bot_perms.update(**perms)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def has_permissions(**perms: bool):
|
||||||
|
"""Restrict the command to users with these permissions.
|
||||||
|
|
||||||
|
This check can be overridden by rules.
|
||||||
|
"""
|
||||||
|
return Requires.get_decorator(None, perms)
|
||||||
|
|
||||||
|
|
||||||
|
def is_owner():
|
||||||
|
"""Restrict the command to bot owners.
|
||||||
|
|
||||||
|
This check cannot be overridden by rules.
|
||||||
|
"""
|
||||||
|
return Requires.get_decorator(PrivilegeLevel.BOT_OWNER, {})
|
||||||
|
|
||||||
|
|
||||||
|
def guildowner_or_permissions(**perms: bool):
|
||||||
|
"""Restrict the command to the guild owner or users with these permissions.
|
||||||
|
|
||||||
|
This check can be overridden by rules.
|
||||||
|
"""
|
||||||
|
return Requires.get_decorator(PrivilegeLevel.GUILD_OWNER, perms)
|
||||||
|
|
||||||
|
|
||||||
|
def guildowner():
|
||||||
|
"""Restrict the command to the guild owner.
|
||||||
|
|
||||||
|
This check can be overridden by rules.
|
||||||
|
"""
|
||||||
|
return guildowner_or_permissions()
|
||||||
|
|
||||||
|
|
||||||
|
def admin_or_permissions(**perms: bool):
|
||||||
|
"""Restrict the command to users with the admin role or these permissions.
|
||||||
|
|
||||||
|
This check can be overridden by rules.
|
||||||
|
"""
|
||||||
|
return Requires.get_decorator(PrivilegeLevel.ADMIN, perms)
|
||||||
|
|
||||||
|
|
||||||
|
def admin():
|
||||||
|
"""Restrict the command to users with the admin role.
|
||||||
|
|
||||||
|
This check can be overridden by rules.
|
||||||
|
"""
|
||||||
|
return admin_or_permissions()
|
||||||
|
|
||||||
|
|
||||||
|
def mod_or_permissions(**perms: bool):
|
||||||
|
"""Restrict the command to users with the mod role or these permissions.
|
||||||
|
|
||||||
|
This check can be overridden by rules.
|
||||||
|
"""
|
||||||
|
return Requires.get_decorator(PrivilegeLevel.MOD, perms)
|
||||||
|
|
||||||
|
|
||||||
|
def mod():
|
||||||
|
"""Restrict the command to users with the mod role.
|
||||||
|
|
||||||
|
This check can be overridden by rules.
|
||||||
|
"""
|
||||||
|
return mod_or_permissions()
|
||||||
|
|
||||||
|
|
||||||
|
class _IntKeyDict(Dict[int, _T]):
|
||||||
|
"""Dict subclass which throws KeyError when a non-int key is used."""
|
||||||
|
|
||||||
|
def __getitem__(self, key: Any) -> _T:
|
||||||
|
if not isinstance(key, int):
|
||||||
|
raise TypeError("Keys must be of type `int`")
|
||||||
|
return super().__getitem__(key)
|
||||||
|
|
||||||
|
def __setitem__(self, key: Any, value: _T) -> None:
|
||||||
|
if not isinstance(key, int):
|
||||||
|
raise TypeError("Keys must be of type `int`")
|
||||||
|
return super().__setitem__(key, value)
|
||||||
@@ -238,6 +238,29 @@ class Group(Value):
|
|||||||
else:
|
else:
|
||||||
return Value(identifiers=new_identifiers, default_value=None, driver=self.driver)
|
return Value(identifiers=new_identifiers, default_value=None, driver=self.driver)
|
||||||
|
|
||||||
|
async def clear_raw(self, *nested_path: str):
|
||||||
|
"""
|
||||||
|
Allows a developer to clear data as if it was stored in a standard
|
||||||
|
Python dictionary.
|
||||||
|
|
||||||
|
For example::
|
||||||
|
|
||||||
|
await conf.clear_raw("foo", "bar")
|
||||||
|
|
||||||
|
# is equivalent to
|
||||||
|
|
||||||
|
data = {"foo": {"bar": None}}
|
||||||
|
del data["foo"]["bar"]
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
nested_path : str
|
||||||
|
Multiple arguments that mirror the arguments passed in for nested
|
||||||
|
dict access.
|
||||||
|
"""
|
||||||
|
path = [str(p) for p in nested_path]
|
||||||
|
await self.driver.clear(*self.identifiers, *path)
|
||||||
|
|
||||||
def is_group(self, item: str) -> bool:
|
def is_group(self, item: str) -> bool:
|
||||||
"""A helper method for `__getattr__`. Most developers will have no need
|
"""A helper method for `__getattr__`. Most developers will have no need
|
||||||
to use this.
|
to use this.
|
||||||
@@ -815,7 +838,7 @@ class Config:
|
|||||||
"""
|
"""
|
||||||
return self._get_base_group(self.ROLE, role.id)
|
return self._get_base_group(self.ROLE, role.id)
|
||||||
|
|
||||||
def user(self, user: discord.User) -> Group:
|
def user(self, user: discord.abc.User) -> Group:
|
||||||
"""Returns a `Group` for the given user.
|
"""Returns a `Group` for the given user.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from pathlib import Path
|
|||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
from string import ascii_letters, digits
|
from string import ascii_letters, digits
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Union
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
@@ -24,6 +24,7 @@ from redbot.core import __version__
|
|||||||
from redbot.core import checks
|
from redbot.core import checks
|
||||||
from redbot.core import i18n
|
from redbot.core import i18n
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
|
from .utils.predicates import MessagePredicate
|
||||||
from .utils.chat_formatting import pagify, box, inline
|
from .utils.chat_formatting import pagify, box, inline
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -241,7 +242,7 @@ class CoreLogic:
|
|||||||
|
|
||||||
|
|
||||||
@i18n.cog_i18n(_)
|
@i18n.cog_i18n(_)
|
||||||
class Core(CoreLogic):
|
class Core(commands.Cog, CoreLogic):
|
||||||
"""Commands related to core functions"""
|
"""Commands related to core functions"""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
@@ -422,7 +423,7 @@ class Core(CoreLogic):
|
|||||||
destination = ctx.channel
|
destination = ctx.channel
|
||||||
|
|
||||||
if self.bot._last_exception:
|
if self.bot._last_exception:
|
||||||
for page in pagify(self.bot._last_exception):
|
for page in pagify(self.bot._last_exception, shorten_by=10):
|
||||||
await destination.send(box(page, lang="py"))
|
await destination.send(box(page, lang="py"))
|
||||||
else:
|
else:
|
||||||
await ctx.send("No exception has occurred yet")
|
await ctx.send("No exception has occurred yet")
|
||||||
@@ -438,73 +439,63 @@ class Core(CoreLogic):
|
|||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def leave(self, ctx):
|
async def leave(self, ctx):
|
||||||
"""Leaves server"""
|
"""Leaves server"""
|
||||||
author = ctx.author
|
await ctx.send("Are you sure you want me to leave this server? (y/n)")
|
||||||
guild = ctx.guild
|
|
||||||
|
|
||||||
await ctx.send("Are you sure you want me to leave this server? Type yes to confirm.")
|
pred = MessagePredicate.yes_or_no(ctx)
|
||||||
|
try:
|
||||||
def conf_check(m):
|
await self.bot.wait_for("message", check=MessagePredicate.yes_or_no(ctx))
|
||||||
return m.author == author
|
except asyncio.TimeoutError:
|
||||||
|
await ctx.send("Response timed out.")
|
||||||
response = await self.bot.wait_for("message", check=conf_check)
|
return
|
||||||
|
else:
|
||||||
if response.content.lower().strip() == "yes":
|
if pred.result is True:
|
||||||
await ctx.send("Alright. Bye :wave:")
|
await ctx.send("Alright. Bye :wave:")
|
||||||
log.debug("Leaving '{}'".format(guild.name))
|
log.debug("Leaving guild '{}'".format(ctx.guild.name))
|
||||||
await guild.leave()
|
await ctx.guild.leave()
|
||||||
|
else:
|
||||||
|
await ctx.send("Alright, I'll stay then :)")
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def servers(self, ctx):
|
async def servers(self, ctx):
|
||||||
"""Lists and allows to leave servers"""
|
"""Lists and allows to leave servers"""
|
||||||
owner = ctx.author
|
|
||||||
guilds = sorted(list(self.bot.guilds), key=lambda s: s.name.lower())
|
guilds = sorted(list(self.bot.guilds), key=lambda s: s.name.lower())
|
||||||
msg = ""
|
msg = ""
|
||||||
|
responses = []
|
||||||
for i, server in enumerate(guilds, 1):
|
for i, server in enumerate(guilds, 1):
|
||||||
msg += "{}: {}\n".format(i, server.name)
|
msg += "{}: {}\n".format(i, server.name)
|
||||||
|
responses.append(str(i))
|
||||||
msg += "\nTo leave a server, just type its number."
|
|
||||||
|
|
||||||
for page in pagify(msg, ["\n"]):
|
for page in pagify(msg, ["\n"]):
|
||||||
await ctx.send(page)
|
await ctx.send(page)
|
||||||
|
|
||||||
def msg_check(m):
|
query = await ctx.send("To leave a server, just type its number.")
|
||||||
return m.author == owner
|
|
||||||
|
|
||||||
while msg is not None:
|
|
||||||
try:
|
|
||||||
msg = await self.bot.wait_for("message", check=msg_check, timeout=15)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
await ctx.send("I guess not.")
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
msg = int(msg.content) - 1
|
|
||||||
if msg < 0:
|
|
||||||
break
|
|
||||||
await self.leave_confirmation(guilds[msg], owner, ctx)
|
|
||||||
break
|
|
||||||
except (IndexError, ValueError, AttributeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def leave_confirmation(self, server, owner, ctx):
|
|
||||||
await ctx.send("Are you sure you want me to leave {}? (yes/no)".format(server.name))
|
|
||||||
|
|
||||||
def conf_check(m):
|
|
||||||
return m.author == owner
|
|
||||||
|
|
||||||
|
pred = MessagePredicate.contained_in(responses, ctx)
|
||||||
try:
|
try:
|
||||||
msg = await self.bot.wait_for("message", check=conf_check, timeout=15)
|
await self.bot.wait_for("message", check=pred, timeout=15)
|
||||||
if msg.content.lower().strip() in ("yes", "y"):
|
except asyncio.TimeoutError:
|
||||||
if server.owner == ctx.bot.user:
|
await query.delete()
|
||||||
await ctx.send("I cannot leave a guild I am the owner of.")
|
else:
|
||||||
return
|
await self.leave_confirmation(guilds[pred.result], ctx)
|
||||||
await server.leave()
|
|
||||||
if server != ctx.guild:
|
async def leave_confirmation(self, guild, ctx):
|
||||||
|
if guild.owner.id == ctx.bot.user.id:
|
||||||
|
await ctx.send("I cannot leave a guild I am the owner of.")
|
||||||
|
return
|
||||||
|
|
||||||
|
await ctx.send("Are you sure you want me to leave {}? (yes/no)".format(guild.name))
|
||||||
|
pred = MessagePredicate.yes_or_no(ctx)
|
||||||
|
try:
|
||||||
|
await self.bot.wait_for("message", check=pred, timeout=15)
|
||||||
|
if pred.result is True:
|
||||||
|
await guild.leave()
|
||||||
|
if guild != ctx.guild:
|
||||||
await ctx.send("Done.")
|
await ctx.send("Done.")
|
||||||
else:
|
else:
|
||||||
await ctx.send("Alright then.")
|
await ctx.send("Alright then.")
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send("I guess not.")
|
await ctx.send("Response timed out.")
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@@ -554,12 +545,10 @@ class Core(CoreLogic):
|
|||||||
|
|
||||||
@commands.command(name="reload")
|
@commands.command(name="reload")
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def reload_(self, ctx, *, cog_name: str):
|
async def reload(self, ctx, *cogs: str):
|
||||||
"""Reloads packages"""
|
"""Reloads packages"""
|
||||||
|
|
||||||
cog_names = [c.strip() for c in cog_name.split(" ")]
|
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
loaded, failed, not_found = await self._reload(cog_names)
|
loaded, failed, not_found = await self._reload(cogs)
|
||||||
|
|
||||||
if loaded:
|
if loaded:
|
||||||
fmt = "Package{plural} {packs} {other} reloaded."
|
fmt = "Package{plural} {packs} {other} reloaded."
|
||||||
@@ -609,10 +598,13 @@ class Core(CoreLogic):
|
|||||||
"""Changes Red's settings"""
|
"""Changes Red's settings"""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
if ctx.guild:
|
if ctx.guild:
|
||||||
admin_role_id = await ctx.bot.db.guild(ctx.guild).admin_role()
|
guild = ctx.guild
|
||||||
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id) or "Not set"
|
admin_role = (
|
||||||
mod_role_id = await ctx.bot.db.guild(ctx.guild).mod_role()
|
guild.get_role(await ctx.bot.db.guild(ctx.guild).admin_role()) or "Not set"
|
||||||
mod_role = discord.utils.get(ctx.guild.roles, id=mod_role_id) or "Not set"
|
)
|
||||||
|
mod_role = (
|
||||||
|
guild.get_role(await ctx.bot.db.guild(ctx.guild).mod_role()) or "Not set"
|
||||||
|
)
|
||||||
prefixes = await ctx.bot.db.guild(ctx.guild).prefix()
|
prefixes = await ctx.bot.db.guild(ctx.guild).prefix()
|
||||||
guild_settings = f"Admin role: {admin_role}\nMod role: {mod_role}\n"
|
guild_settings = f"Admin role: {admin_role}\nMod role: {mod_role}\n"
|
||||||
else:
|
else:
|
||||||
@@ -889,10 +881,6 @@ class Core(CoreLogic):
|
|||||||
@commands.cooldown(1, 60 * 10, commands.BucketType.default)
|
@commands.cooldown(1, 60 * 10, commands.BucketType.default)
|
||||||
async def owner(self, ctx):
|
async def owner(self, ctx):
|
||||||
"""Sets Red's main owner"""
|
"""Sets Red's main owner"""
|
||||||
|
|
||||||
def check(m):
|
|
||||||
return m.author == ctx.author and m.channel == ctx.channel
|
|
||||||
|
|
||||||
# According to the Python docs this is suitable for cryptographic use
|
# According to the Python docs this is suitable for cryptographic use
|
||||||
random = SystemRandom()
|
random = SystemRandom()
|
||||||
length = random.randint(25, 35)
|
length = random.randint(25, 35)
|
||||||
@@ -916,10 +904,14 @@ class Core(CoreLogic):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = await ctx.bot.wait_for("message", check=check, timeout=60)
|
message = await ctx.bot.wait_for(
|
||||||
|
"message", check=MessagePredicate.same_context(ctx), timeout=60
|
||||||
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
self.owner.reset_cooldown(ctx)
|
self.owner.reset_cooldown(ctx)
|
||||||
await ctx.send(_("The set owner request has timed out."))
|
await ctx.send(
|
||||||
|
_("The `{prefix}set owner` request has timed out.").format(prefix=ctx.prefix)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if message.content.strip() == token:
|
if message.content.strip() == token:
|
||||||
self.owner.reset_cooldown(ctx)
|
self.owner.reset_cooldown(ctx)
|
||||||
@@ -1070,7 +1062,7 @@ class Core(CoreLogic):
|
|||||||
if not locale_list:
|
if not locale_list:
|
||||||
await ctx.send("No languages found.")
|
await ctx.send("No languages found.")
|
||||||
return
|
return
|
||||||
pages = pagify("\n".join(locale_list))
|
pages = pagify("\n".join(locale_list), shorten_by=26)
|
||||||
|
|
||||||
await ctx.send_interactive(pages, box_lang="Available Locales:")
|
await ctx.send_interactive(pages, box_lang="Available Locales:")
|
||||||
|
|
||||||
@@ -1143,18 +1135,20 @@ class Core(CoreLogic):
|
|||||||
)
|
)
|
||||||
await ctx.send(_("Would you like to receive a copy via DM? (y/n)"))
|
await ctx.send(_("Would you like to receive a copy via DM? (y/n)"))
|
||||||
|
|
||||||
def same_author_check(m):
|
pred = MessagePredicate.yes_or_no(ctx)
|
||||||
return m.author == ctx.author and m.channel == ctx.channel
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=60)
|
await ctx.bot.wait_for("message", check=pred, timeout=60)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send(_("Ok then."))
|
await ctx.send(_("Response timed out."))
|
||||||
else:
|
else:
|
||||||
if msg.content.lower().strip() == "y":
|
if pred.result is True:
|
||||||
await ctx.author.send(
|
await ctx.send(_("OK, it's on its way!"))
|
||||||
_("Here's a copy of the backup"), file=discord.File(str(backup_file))
|
async with ctx.author.typing():
|
||||||
)
|
await ctx.author.send(
|
||||||
|
_("Here's a copy of the backup"), file=discord.File(str(backup_file))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await ctx.send(_("OK then."))
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("That directory doesn't seem to exist..."))
|
await ctx.send(_("That directory doesn't seem to exist..."))
|
||||||
|
|
||||||
@@ -1694,6 +1688,82 @@ class Core(CoreLogic):
|
|||||||
await ctx.bot.db.disabled_command_msg.set(message)
|
await ctx.bot.db.disabled_command_msg.set(message)
|
||||||
await ctx.tick()
|
await ctx.tick()
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@checks.guildowner_or_permissions(manage_server=True)
|
||||||
|
@commands.group(name="autoimmune")
|
||||||
|
async def autoimmune_group(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Server settings for immunity from automated actions
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@autoimmune_group.command(name="list")
|
||||||
|
async def autoimmune_list(self, ctx: commands.Context):
|
||||||
|
"""
|
||||||
|
Get's the current members and roles
|
||||||
|
|
||||||
|
configured for automatic moderation action immunity
|
||||||
|
"""
|
||||||
|
ai_ids = await ctx.bot.db.guild(ctx.guild).autoimmune_ids()
|
||||||
|
|
||||||
|
roles = {r.name for r in ctx.guild.roles if r.id in ai_ids}
|
||||||
|
members = {str(m) for m in ctx.guild.members if m.id in ai_ids}
|
||||||
|
|
||||||
|
output = ""
|
||||||
|
if roles:
|
||||||
|
output += _("Roles immune from automated moderation actions:\n")
|
||||||
|
output += ", ".join(roles)
|
||||||
|
if members:
|
||||||
|
if roles:
|
||||||
|
output += "\n"
|
||||||
|
output += _("Members immune from automated moderation actions:\n")
|
||||||
|
output += ", ".join(members)
|
||||||
|
|
||||||
|
if not output:
|
||||||
|
output = _("No immunty settings here.")
|
||||||
|
|
||||||
|
for page in pagify(output):
|
||||||
|
await ctx.send(page)
|
||||||
|
|
||||||
|
@autoimmune_group.command(name="add")
|
||||||
|
async def autoimmune_add(
|
||||||
|
self, ctx: commands.Context, user_or_role: Union[discord.Member, discord.Role]
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Makes a user or roles immune from automated moderation actions
|
||||||
|
"""
|
||||||
|
async with ctx.bot.db.guild(ctx.guild).autoimmune_ids() as ai_ids:
|
||||||
|
if user_or_role.id in ai_ids:
|
||||||
|
return await ctx.send(_("Already added."))
|
||||||
|
ai_ids.append(user_or_role.id)
|
||||||
|
await ctx.tick()
|
||||||
|
|
||||||
|
@autoimmune_group.command(name="remove")
|
||||||
|
async def autoimmune_remove(
|
||||||
|
self, ctx: commands.Context, user_or_role: Union[discord.Member, discord.Role]
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Makes a user or roles immune from automated moderation actions
|
||||||
|
"""
|
||||||
|
async with ctx.bot.db.guild(ctx.guild).autoimmune_ids() as ai_ids:
|
||||||
|
if user_or_role.id not in ai_ids:
|
||||||
|
return await ctx.send(_("Not in list."))
|
||||||
|
ai_ids.remove(user_or_role.id)
|
||||||
|
await ctx.tick()
|
||||||
|
|
||||||
|
@autoimmune_group.command(name="isimmune")
|
||||||
|
async def autoimmune_checkimmune(
|
||||||
|
self, ctx: commands.Context, user_or_role: Union[discord.Member, discord.Role]
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Checks if a user or role would be considered immune from automated actions
|
||||||
|
"""
|
||||||
|
|
||||||
|
if await ctx.bot.is_automod_immune(user_or_role):
|
||||||
|
await ctx.send(_("They are immune"))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("They are not Immune"))
|
||||||
|
|
||||||
# RPC handlers
|
# RPC handlers
|
||||||
async def rpc_load(self, request):
|
async def rpc_load(self, request):
|
||||||
cog_name = request.params[0]
|
cog_name = request.params[0]
|
||||||
@@ -1704,7 +1774,7 @@ class Core(CoreLogic):
|
|||||||
|
|
||||||
self._cleanup_and_refresh_modules(spec.name)
|
self._cleanup_and_refresh_modules(spec.name)
|
||||||
|
|
||||||
self.bot.load_extension(spec)
|
await self.bot.load_extension(spec)
|
||||||
|
|
||||||
async def rpc_unload(self, request):
|
async def rpc_unload(self, request):
|
||||||
cog_name = request.params[0]
|
cog_name = request.params[0]
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ from contextlib import redirect_stdout
|
|||||||
from copy import copy
|
from copy import copy
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from . import checks, commands
|
from . import checks, commands
|
||||||
from .i18n import Translator
|
from .i18n import Translator
|
||||||
from .utils.chat_formatting import box, pagify
|
from .utils.chat_formatting import box, pagify
|
||||||
|
from .utils.predicates import MessagePredicate
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Notice:
|
Notice:
|
||||||
@@ -25,10 +27,11 @@ _ = Translator("Dev", __file__)
|
|||||||
START_CODE_BLOCK_RE = re.compile(r"^((```py)(?=\s)|(```))")
|
START_CODE_BLOCK_RE = re.compile(r"^((```py)(?=\s)|(```))")
|
||||||
|
|
||||||
|
|
||||||
class Dev:
|
class Dev(commands.Cog):
|
||||||
"""Various development focused utilities."""
|
"""Various development focused utilities."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self._last_result = None
|
self._last_result = None
|
||||||
self.sessions = set()
|
self.sessions = set()
|
||||||
|
|
||||||
@@ -217,12 +220,8 @@ class Dev:
|
|||||||
self.sessions.add(ctx.channel.id)
|
self.sessions.add(ctx.channel.id)
|
||||||
await ctx.send(_("Enter code to execute or evaluate. `exit()` or `quit` to exit."))
|
await ctx.send(_("Enter code to execute or evaluate. `exit()` or `quit` to exit."))
|
||||||
|
|
||||||
msg_check = lambda m: (
|
|
||||||
m.author == ctx.author and m.channel == ctx.channel and m.content.startswith("`")
|
|
||||||
)
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
response = await ctx.bot.wait_for("message", check=msg_check)
|
response = await ctx.bot.wait_for("message", check=MessagePredicate.regex(r"^`", ctx))
|
||||||
|
|
||||||
cleaned = self.cleanup_code(response.content)
|
cleaned = self.cleanup_code(response.content)
|
||||||
|
|
||||||
|
|||||||
@@ -9,18 +9,24 @@ _conn = None
|
|||||||
|
|
||||||
|
|
||||||
def _initialize(**kwargs):
|
def _initialize(**kwargs):
|
||||||
|
kwargs.get("URI", "mongodb")
|
||||||
host = kwargs["HOST"]
|
host = kwargs["HOST"]
|
||||||
port = kwargs["PORT"]
|
port = kwargs["PORT"]
|
||||||
admin_user = kwargs["USERNAME"]
|
admin_user = kwargs["USERNAME"]
|
||||||
admin_pass = kwargs["PASSWORD"]
|
admin_pass = kwargs["PASSWORD"]
|
||||||
db_name = kwargs.get("DB_NAME", "default_db")
|
db_name = kwargs.get("DB_NAME", "default_db")
|
||||||
|
|
||||||
|
if port is 0:
|
||||||
|
ports = ""
|
||||||
|
else:
|
||||||
|
ports = ":{}".format(port)
|
||||||
|
|
||||||
if admin_user is not None and admin_pass is not None:
|
if admin_user is not None and admin_pass is not None:
|
||||||
url = "mongodb://{}:{}@{}:{}/{}".format(
|
url = "{}://{}:{}@{}{}/{}".format(
|
||||||
quote_plus(admin_user), quote_plus(admin_pass), host, port, db_name
|
uri, quote_plus(admin_user), quote_plus(admin_pass), host, ports, db_name
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
url = "mongodb://{}:{}/{}".format(host, port, db_name)
|
url = "{}://{}{}/{}".format(uri, host, ports, db_name)
|
||||||
|
|
||||||
global _conn
|
global _conn
|
||||||
_conn = motor.motor_asyncio.AsyncIOMotorClient(url)
|
_conn = motor.motor_asyncio.AsyncIOMotorClient(url)
|
||||||
@@ -111,8 +117,22 @@ class Mongo(BaseDriver):
|
|||||||
|
|
||||||
|
|
||||||
def get_config_details():
|
def get_config_details():
|
||||||
|
uri = None
|
||||||
|
while True:
|
||||||
|
uri = input("Enter URI scheme (mongodb or mongodb+srv): ")
|
||||||
|
if uri is "":
|
||||||
|
uri = "mongodb"
|
||||||
|
|
||||||
|
if uri in ["mongodb", "mongodb+srv"]:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("Invalid URI scheme")
|
||||||
|
|
||||||
host = input("Enter host address: ")
|
host = input("Enter host address: ")
|
||||||
port = int(input("Enter host port: "))
|
if uri is "mongodb":
|
||||||
|
port = int(input("Enter host port: "))
|
||||||
|
else:
|
||||||
|
port = 0
|
||||||
|
|
||||||
admin_uname = input("Enter login username: ")
|
admin_uname = input("Enter login username: ")
|
||||||
admin_password = input("Enter login password: ")
|
admin_password = input("Enter login password: ")
|
||||||
@@ -128,5 +148,6 @@ def get_config_details():
|
|||||||
"USERNAME": admin_uname,
|
"USERNAME": admin_uname,
|
||||||
"PASSWORD": admin_password,
|
"PASSWORD": admin_password,
|
||||||
"DB_NAME": db_name,
|
"DB_NAME": db_name,
|
||||||
|
"URI": uri,
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -2,20 +2,21 @@ import sys
|
|||||||
import codecs
|
import codecs
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
import traceback
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
import traceback
|
|
||||||
from colorama import Fore, Style, init
|
from colorama import Fore, Style, init
|
||||||
from pkg_resources import DistributionNotFound
|
from pkg_resources import DistributionNotFound
|
||||||
|
|
||||||
from . import __version__, commands
|
from . import __version__, commands
|
||||||
from .data_manager import storage_type
|
from .data_manager import storage_type
|
||||||
from .utils.chat_formatting import inline, bordered
|
from .utils.chat_formatting import inline, bordered, humanize_list
|
||||||
from .utils import fuzzy_command_search
|
from .utils import fuzzy_command_search, format_fuzzy_results
|
||||||
|
|
||||||
log = logging.getLogger("red")
|
log = logging.getLogger("red")
|
||||||
sentry_log = logging.getLogger("red.sentry")
|
sentry_log = logging.getLogger("red.sentry")
|
||||||
@@ -43,7 +44,7 @@ def should_log_sentry(exception) -> bool:
|
|||||||
tb = tb.tb_next
|
tb = tb.tb_next
|
||||||
|
|
||||||
module = tb_frame.f_globals.get("__name__")
|
module = tb_frame.f_globals.get("__name__")
|
||||||
return module.startswith("redbot")
|
return module is not None and module.startswith("redbot")
|
||||||
|
|
||||||
|
|
||||||
def init_events(bot, cli_flags):
|
def init_events(bot, cli_flags):
|
||||||
@@ -67,6 +68,14 @@ def init_events(bot, cli_flags):
|
|||||||
packages.extend(cli_flags.load_cogs)
|
packages.extend(cli_flags.load_cogs)
|
||||||
|
|
||||||
if packages:
|
if packages:
|
||||||
|
# Load permissions first, for security reasons
|
||||||
|
try:
|
||||||
|
packages.remove("permissions")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
packages.insert(0, "permissions")
|
||||||
|
|
||||||
to_remove = []
|
to_remove = []
|
||||||
print("Loading packages...")
|
print("Loading packages...")
|
||||||
for package in packages:
|
for package in packages:
|
||||||
@@ -197,17 +206,6 @@ def init_events(bot, cli_flags):
|
|||||||
if disabled_message:
|
if disabled_message:
|
||||||
await ctx.send(disabled_message.replace("{command}", ctx.invoked_with))
|
await ctx.send(disabled_message.replace("{command}", ctx.invoked_with))
|
||||||
elif isinstance(error, commands.CommandInvokeError):
|
elif isinstance(error, commands.CommandInvokeError):
|
||||||
# Need to test if the following still works
|
|
||||||
"""
|
|
||||||
no_dms = "Cannot send messages to this user"
|
|
||||||
is_help_cmd = ctx.command.qualified_name == "help"
|
|
||||||
is_forbidden = isinstance(error.original, discord.Forbidden)
|
|
||||||
if is_help_cmd and is_forbidden and error.original.text == no_dms:
|
|
||||||
msg = ("I couldn't send the help message to you in DM. Either"
|
|
||||||
" you blocked me or you disabled DMs in this server.")
|
|
||||||
await ctx.send(msg)
|
|
||||||
return
|
|
||||||
"""
|
|
||||||
log.exception(
|
log.exception(
|
||||||
"Exception in command '{}'" "".format(ctx.command.qualified_name),
|
"Exception in command '{}'" "".format(ctx.command.qualified_name),
|
||||||
exc_info=error.original,
|
exc_info=error.original,
|
||||||
@@ -231,12 +229,28 @@ def init_events(bot, cli_flags):
|
|||||||
if not hasattr(ctx.cog, "_{0.command.cog_name}__error".format(ctx)):
|
if not hasattr(ctx.cog, "_{0.command.cog_name}__error".format(ctx)):
|
||||||
await ctx.send(inline(message))
|
await ctx.send(inline(message))
|
||||||
elif isinstance(error, commands.CommandNotFound):
|
elif isinstance(error, commands.CommandNotFound):
|
||||||
term = ctx.invoked_with + " "
|
fuzzy_commands = await fuzzy_command_search(ctx)
|
||||||
if len(ctx.args) > 1:
|
if not fuzzy_commands:
|
||||||
term += " ".join(ctx.args[1:])
|
pass
|
||||||
fuzzy_result = await fuzzy_command_search(ctx, ctx.invoked_with)
|
elif await ctx.embed_requested():
|
||||||
if fuzzy_result is not None:
|
await ctx.send(embed=await format_fuzzy_results(ctx, fuzzy_commands, embed=True))
|
||||||
await ctx.maybe_send_embed(fuzzy_result)
|
else:
|
||||||
|
await ctx.send(await format_fuzzy_results(ctx, fuzzy_commands, embed=False))
|
||||||
|
elif isinstance(error, commands.BotMissingPermissions):
|
||||||
|
missing_perms: List[str] = []
|
||||||
|
for perm, value in error.missing:
|
||||||
|
if value is True:
|
||||||
|
perm_name = '"' + perm.replace("_", " ").title() + '"'
|
||||||
|
missing_perms.append(perm_name)
|
||||||
|
if len(missing_perms) == 1:
|
||||||
|
plural = ""
|
||||||
|
else:
|
||||||
|
plural = "s"
|
||||||
|
await ctx.send(
|
||||||
|
"I require the {perms} permission{plural} to execute that command.".format(
|
||||||
|
perms=humanize_list(missing_perms), plural=plural
|
||||||
|
)
|
||||||
|
)
|
||||||
elif isinstance(error, commands.CheckFailure):
|
elif isinstance(error, commands.CheckFailure):
|
||||||
pass
|
pass
|
||||||
elif isinstance(error, commands.NoPrivateMessage):
|
elif isinstance(error, commands.NoPrivateMessage):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from . import commands
|
|||||||
|
|
||||||
|
|
||||||
def init_global_checks(bot):
|
def init_global_checks(bot):
|
||||||
@bot.check
|
@bot.check_once
|
||||||
async def global_perms(ctx):
|
async def global_perms(ctx):
|
||||||
"""Check the user is/isn't globally whitelisted/blacklisted."""
|
"""Check the user is/isn't globally whitelisted/blacklisted."""
|
||||||
if await bot.is_owner(ctx.author):
|
if await bot.is_owner(ctx.author):
|
||||||
@@ -15,7 +15,7 @@ def init_global_checks(bot):
|
|||||||
|
|
||||||
return ctx.author.id not in await bot.db.blacklist()
|
return ctx.author.id not in await bot.db.blacklist()
|
||||||
|
|
||||||
@bot.check
|
@bot.check_once
|
||||||
async def local_perms(ctx: commands.Context):
|
async def local_perms(ctx: commands.Context):
|
||||||
"""Check the user is/isn't locally whitelisted/blacklisted."""
|
"""Check the user is/isn't locally whitelisted/blacklisted."""
|
||||||
if await bot.is_owner(ctx.author):
|
if await bot.is_owner(ctx.author):
|
||||||
@@ -26,14 +26,14 @@ def init_global_checks(bot):
|
|||||||
local_blacklist = await guild_settings.blacklist()
|
local_blacklist = await guild_settings.blacklist()
|
||||||
local_whitelist = await guild_settings.whitelist()
|
local_whitelist = await guild_settings.whitelist()
|
||||||
|
|
||||||
_ids = [r.id for r in ctx.author.roles if not r.is_default]
|
_ids = [r.id for r in ctx.author.roles if not r.is_default()]
|
||||||
_ids.append(ctx.author.id)
|
_ids.append(ctx.author.id)
|
||||||
if local_whitelist:
|
if local_whitelist:
|
||||||
return any(i in local_whitelist for i in _ids)
|
return any(i in local_whitelist for i in _ids)
|
||||||
|
|
||||||
return not any(i in local_blacklist for i in _ids)
|
return not any(i in local_blacklist for i in _ids)
|
||||||
|
|
||||||
@bot.check
|
@bot.check_once
|
||||||
async def bots(ctx):
|
async def bots(ctx):
|
||||||
"""Check the user is not another bot."""
|
"""Check the user is not another bot."""
|
||||||
return not ctx.author.bot
|
return not ctx.author.bot
|
||||||
|
|||||||
@@ -20,25 +20,24 @@ message to help page.
|
|||||||
e.g. format_help_for(ctx, ctx.command, "Missing required arguments")
|
e.g. format_help_for(ctx, ctx.command, "Missing required arguments")
|
||||||
|
|
||||||
discord.py 1.0.0a
|
discord.py 1.0.0a
|
||||||
Experimental: compatibility with 0.16.8
|
|
||||||
|
|
||||||
Copyrights to logic of code belong to Rapptz (Danny)
|
This help formatter contains work by Rapptz (Danny) and SirThane#1780.
|
||||||
Everything else credit to SirThane#1780"""
|
"""
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import List
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext.commands import formatter
|
from discord.ext.commands import formatter as dpy_formatter
|
||||||
import inspect
|
import inspect
|
||||||
import itertools
|
import itertools
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from . import commands
|
from . import commands
|
||||||
from redbot.core.utils.chat_formatting import pagify, box
|
from .i18n import Translator
|
||||||
from redbot.core.utils import fuzzy_command_search
|
from .utils.chat_formatting import pagify
|
||||||
|
from .utils import fuzzy_command_search, format_fuzzy_results
|
||||||
|
|
||||||
|
_ = Translator("Help", __file__)
|
||||||
|
|
||||||
EMPTY_STRING = "\u200b"
|
EMPTY_STRING = "\u200b"
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ _mention_pattern = re.compile("|".join(_mentions_transforms.keys()))
|
|||||||
EmbedField = namedtuple("EmbedField", "name value inline")
|
EmbedField = namedtuple("EmbedField", "name value inline")
|
||||||
|
|
||||||
|
|
||||||
class Help(formatter.HelpFormatter):
|
class Help(dpy_formatter.HelpFormatter):
|
||||||
"""Formats help for commands."""
|
"""Formats help for commands."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -57,15 +56,10 @@ class Help(formatter.HelpFormatter):
|
|||||||
self.command = None
|
self.command = None
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def pm_check(self, ctx):
|
@staticmethod
|
||||||
|
def pm_check(ctx):
|
||||||
return isinstance(ctx.channel, discord.DMChannel)
|
return isinstance(ctx.channel, discord.DMChannel)
|
||||||
|
|
||||||
@property
|
|
||||||
def clean_prefix(self):
|
|
||||||
maybe_member = self.context.guild.me if self.context.guild else self.context.bot.user
|
|
||||||
pretty = f"@{maybe_member.display_name}"
|
|
||||||
return self.context.prefix.replace(maybe_member.mention, pretty)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def me(self):
|
def me(self):
|
||||||
return self.context.me
|
return self.context.me
|
||||||
@@ -84,6 +78,8 @@ class Help(formatter.HelpFormatter):
|
|||||||
else:
|
else:
|
||||||
return await self.context.embed_colour()
|
return await self.context.embed_colour()
|
||||||
|
|
||||||
|
colour = color
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def destination(self):
|
def destination(self):
|
||||||
if self.context.bot.pm_help:
|
if self.context.bot.pm_help:
|
||||||
@@ -110,7 +106,7 @@ class Help(formatter.HelpFormatter):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if self.is_cog() or self.is_bot():
|
if self.is_cog() or self.is_bot():
|
||||||
name = "{0}{1}".format(self.clean_prefix, name)
|
name = "{0}{1}".format(self.context.clean_prefix, name)
|
||||||
|
|
||||||
entries += "**{0}** {1}\n".format(name, command.short_doc)
|
entries += "**{0}** {1}\n".format(name, command.short_doc)
|
||||||
return entries
|
return entries
|
||||||
@@ -120,7 +116,7 @@ class Help(formatter.HelpFormatter):
|
|||||||
return (
|
return (
|
||||||
"Type {0}help <command> for more info on a command.\n"
|
"Type {0}help <command> for more info on a command.\n"
|
||||||
"You can also type {0}help <category> for more info on a category.".format(
|
"You can also type {0}help <category> for more info on a category.".format(
|
||||||
self.clean_prefix
|
self.context.clean_prefix
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -163,7 +159,7 @@ class Help(formatter.HelpFormatter):
|
|||||||
if self.command.help:
|
if self.command.help:
|
||||||
splitted = self.command.help.split("\n\n")
|
splitted = self.command.help.split("\n\n")
|
||||||
name = "__{0}__".format(splitted[0])
|
name = "__{0}__".format(splitted[0])
|
||||||
value = "\n\n".join(splitted[1:]).replace("[p]", self.clean_prefix)
|
value = "\n\n".join(splitted[1:]).replace("[p]", self.context.clean_prefix)
|
||||||
if value == "":
|
if value == "":
|
||||||
value = EMPTY_STRING
|
value = EMPTY_STRING
|
||||||
field = EmbedField(name[:252], value[:1024], False)
|
field = EmbedField(name[:252], value[:1024], False)
|
||||||
@@ -213,7 +209,8 @@ class Help(formatter.HelpFormatter):
|
|||||||
|
|
||||||
return emb
|
return emb
|
||||||
|
|
||||||
def group_fields(self, fields: List[EmbedField], max_chars=1000):
|
@staticmethod
|
||||||
|
def group_fields(fields: List[EmbedField], max_chars=1000):
|
||||||
curr_group = []
|
curr_group = []
|
||||||
ret = []
|
ret = []
|
||||||
for f in fields:
|
for f in fields:
|
||||||
@@ -277,158 +274,115 @@ class Help(formatter.HelpFormatter):
|
|||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
async def simple_embed(self, ctx, title=None, description=None, color=None):
|
async def format_command_not_found(
|
||||||
# Shortcut
|
self, ctx: commands.Context, command_name: str
|
||||||
|
) -> Optional[Union[str, discord.Message]]:
|
||||||
|
"""Get the response for a user calling help on a missing command."""
|
||||||
self.context = ctx
|
self.context = ctx
|
||||||
if color is None:
|
return await default_command_not_found(
|
||||||
color = await self.color()
|
ctx,
|
||||||
embed = discord.Embed(title=title, description=description, color=color)
|
command_name,
|
||||||
embed.set_footer(text=ctx.bot.formatter.get_ending_note())
|
use_embeds=True,
|
||||||
embed.set_author(**self.author)
|
colour=await self.colour(),
|
||||||
return embed
|
author=self.author,
|
||||||
|
footer={"text": self.get_ending_note()},
|
||||||
async def cmd_not_found(self, ctx, cmd, description=None, color=None):
|
|
||||||
# Shortcut for a shortcut. Sue me
|
|
||||||
embed = await self.simple_embed(
|
|
||||||
ctx, title="Command {} not found.".format(cmd), description=description, color=color
|
|
||||||
)
|
)
|
||||||
return embed
|
|
||||||
|
|
||||||
async def cmd_has_no_subcommands(self, ctx, cmd, color=None):
|
|
||||||
embed = await self.simple_embed(
|
|
||||||
ctx, title=ctx.bot.command_has_no_subcommands.format(cmd), color=color
|
|
||||||
)
|
|
||||||
return embed
|
|
||||||
|
|
||||||
|
|
||||||
@commands.command(hidden=True)
|
@commands.command(hidden=True)
|
||||||
async def help(ctx, *cmds: str):
|
async def help(ctx: commands.Context, *, command_name: str = ""):
|
||||||
"""Shows help documentation.
|
"""Show help documentation.
|
||||||
|
|
||||||
[p]**help**: Shows the help manual.
|
- `[p]help`: Show the help manual.
|
||||||
[p]**help** command: Show help for a command
|
- `[p]help command`: Show help for a command.
|
||||||
[p]**help** Category: Show commands and description for a category"""
|
- `[p]help Category`: Show commands and description for a category,
|
||||||
destination = ctx.author if ctx.bot.pm_help else ctx
|
"""
|
||||||
|
bot = ctx.bot
|
||||||
def repl(obj):
|
if bot.pm_help:
|
||||||
return _mentions_transforms.get(obj.group(0), "")
|
destination = ctx.author
|
||||||
|
else:
|
||||||
|
destination = ctx.channel
|
||||||
|
|
||||||
use_embeds = await ctx.embed_requested()
|
use_embeds = await ctx.embed_requested()
|
||||||
f = formatter.HelpFormatter()
|
if use_embeds:
|
||||||
# help by itself just lists our own commands.
|
formatter = bot.formatter
|
||||||
if len(cmds) == 0:
|
|
||||||
if use_embeds:
|
|
||||||
embeds = await ctx.bot.formatter.format_help_for(ctx, ctx.bot)
|
|
||||||
else:
|
|
||||||
embeds = await f.format_help_for(ctx, ctx.bot)
|
|
||||||
elif len(cmds) == 1:
|
|
||||||
# try to see if it is a cog name
|
|
||||||
name = _mention_pattern.sub(repl, cmds[0])
|
|
||||||
command = None
|
|
||||||
if name in ctx.bot.cogs:
|
|
||||||
command = ctx.bot.cogs[name]
|
|
||||||
else:
|
|
||||||
command = ctx.bot.all_commands.get(name)
|
|
||||||
if command is None:
|
|
||||||
if use_embeds:
|
|
||||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
|
||||||
if fuzzy_result is not None:
|
|
||||||
await destination.send(
|
|
||||||
embed=await ctx.bot.formatter.cmd_not_found(
|
|
||||||
ctx, name, description=fuzzy_result
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
|
||||||
if fuzzy_result is not None:
|
|
||||||
await destination.send(
|
|
||||||
ctx.bot.command_not_found.format(name, fuzzy_result)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if use_embeds:
|
|
||||||
embeds = await ctx.bot.formatter.format_help_for(ctx, command)
|
|
||||||
else:
|
|
||||||
embeds = await f.format_help_for(ctx, command)
|
|
||||||
else:
|
else:
|
||||||
name = _mention_pattern.sub(repl, cmds[0])
|
formatter = dpy_formatter.HelpFormatter()
|
||||||
command = ctx.bot.all_commands.get(name)
|
|
||||||
if command is None:
|
|
||||||
if use_embeds:
|
|
||||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
|
||||||
if fuzzy_result is not None:
|
|
||||||
await destination.send(
|
|
||||||
embed=await ctx.bot.formatter.cmd_not_found(
|
|
||||||
ctx, name, description=fuzzy_result
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
|
||||||
if fuzzy_result is not None:
|
|
||||||
await destination.send(ctx.bot.command_not_found.format(name, fuzzy_result))
|
|
||||||
return
|
|
||||||
|
|
||||||
for key in cmds[1:]:
|
if not command_name:
|
||||||
try:
|
# help by itself just lists our own commands.
|
||||||
key = _mention_pattern.sub(repl, key)
|
pages = await formatter.format_help_for(ctx, bot)
|
||||||
command = command.all_commands.get(key)
|
else:
|
||||||
if command is None:
|
# First check if it's a cog
|
||||||
if use_embeds:
|
command = bot.get_cog(command_name)
|
||||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
if command is None:
|
||||||
if fuzzy_result is not None:
|
command = bot.get_command(command_name)
|
||||||
await destination.send(
|
if command is None:
|
||||||
embed=await ctx.bot.formatter.cmd_not_found(
|
if hasattr(formatter, "format_command_not_found"):
|
||||||
ctx, name, description=fuzzy_result
|
msg = await formatter.format_command_not_found(ctx, command_name)
|
||||||
)
|
else:
|
||||||
)
|
msg = await default_command_not_found(ctx, command_name, use_embeds=use_embeds)
|
||||||
else:
|
pages = [msg]
|
||||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
|
||||||
if fuzzy_result is not None:
|
|
||||||
await destination.send(
|
|
||||||
ctx.bot.command_not_found.format(name, fuzzy_result)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
except AttributeError:
|
|
||||||
if use_embeds:
|
|
||||||
await destination.send(
|
|
||||||
embed=await ctx.bot.formatter.simple_embed(
|
|
||||||
ctx,
|
|
||||||
title='Command "{0.name}" has no subcommands.'.format(command),
|
|
||||||
color=await ctx.bot.formatter.color(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await destination.send(ctx.bot.command_has_no_subcommands.format(command))
|
|
||||||
return
|
|
||||||
if use_embeds:
|
|
||||||
embeds = await ctx.bot.formatter.format_help_for(ctx, command)
|
|
||||||
else:
|
else:
|
||||||
embeds = await f.format_help_for(ctx, command)
|
pages = await formatter.format_help_for(ctx, command)
|
||||||
|
|
||||||
max_pages_in_guild = await ctx.bot.db.help.max_pages_in_guild()
|
max_pages_in_guild = await ctx.bot.db.help.max_pages_in_guild()
|
||||||
if len(embeds) > max_pages_in_guild:
|
if len(pages) > max_pages_in_guild:
|
||||||
|
destination = ctx.author
|
||||||
|
if ctx.guild and not ctx.guild.me.permissions_in(ctx.channel).send_messages:
|
||||||
destination = ctx.author
|
destination = ctx.author
|
||||||
try:
|
try:
|
||||||
for embed in embeds:
|
for page in pages:
|
||||||
if use_embeds:
|
if isinstance(page, discord.Embed):
|
||||||
try:
|
await destination.send(embed=page)
|
||||||
await destination.send(embed=embed)
|
|
||||||
except discord.HTTPException:
|
|
||||||
destination = ctx.author
|
|
||||||
await destination.send(embed=embed)
|
|
||||||
else:
|
else:
|
||||||
try:
|
await destination.send(page)
|
||||||
await destination.send(embed)
|
|
||||||
except discord.HTTPException:
|
|
||||||
destination = ctx.author
|
|
||||||
await destination.send(embed)
|
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.channel.send(
|
await ctx.channel.send(
|
||||||
"I couldn't send the help message to you in DM. Either you blocked me or you disabled DMs in this server."
|
_(
|
||||||
|
"I couldn't send the help message to you in DM. Either you blocked me or you "
|
||||||
|
"disabled DMs in this server."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@help.error
|
async def default_command_not_found(
|
||||||
async def help_error(ctx, error):
|
ctx: commands.Context, command_name: str, *, use_embeds: bool, **embed_options
|
||||||
destination = ctx.author if ctx.bot.pm_help else ctx
|
) -> Optional[Union[str, discord.Embed]]:
|
||||||
await destination.send("{0.__name__}: {1}".format(type(error), error))
|
"""Default function for formatting the response to a missing command."""
|
||||||
traceback.print_tb(error.original.__traceback__, file=sys.stderr)
|
ret = None
|
||||||
|
cmds = command_name.split()
|
||||||
|
prev_command = None
|
||||||
|
for invoked in itertools.accumulate(cmds, lambda *args: " ".join(args)):
|
||||||
|
command = ctx.bot.get_command(invoked)
|
||||||
|
if command is None:
|
||||||
|
if prev_command is not None and not isinstance(prev_command, commands.Group):
|
||||||
|
ret = _("Command *{command_name}* has no subcommands.").format(
|
||||||
|
command_name=prev_command.qualified_name
|
||||||
|
)
|
||||||
|
break
|
||||||
|
elif not await command.can_see(ctx):
|
||||||
|
return
|
||||||
|
prev_command = command
|
||||||
|
|
||||||
|
if ret is None:
|
||||||
|
fuzzy_commands = await fuzzy_command_search(ctx, command_name, min_score=75)
|
||||||
|
if fuzzy_commands:
|
||||||
|
ret = await format_fuzzy_results(ctx, fuzzy_commands, embed=use_embeds)
|
||||||
|
else:
|
||||||
|
ret = _("Command *{command_name}* not found.").format(command_name=command_name)
|
||||||
|
|
||||||
|
if use_embeds:
|
||||||
|
if isinstance(ret, str):
|
||||||
|
ret = discord.Embed(title=ret)
|
||||||
|
if "colour" in embed_options:
|
||||||
|
ret.colour = embed_options.pop("colour")
|
||||||
|
elif "color" in embed_options:
|
||||||
|
ret.colour = embed_options.pop("color")
|
||||||
|
|
||||||
|
if "author" in embed_options:
|
||||||
|
ret.set_author(**embed_options.pop("author"))
|
||||||
|
if "footer" in embed_options:
|
||||||
|
ret.set_footer(**embed_options.pop("footer"))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Callable, Union
|
||||||
|
|
||||||
from . import commands
|
from . import commands
|
||||||
|
|
||||||
@@ -113,9 +115,9 @@ def _normalize(string, remove_newline=False):
|
|||||||
ends_with_space = s[-1] in " \n\t\r"
|
ends_with_space = s[-1] in " \n\t\r"
|
||||||
if remove_newline:
|
if remove_newline:
|
||||||
newline_re = re.compile("[\r\n]+")
|
newline_re = re.compile("[\r\n]+")
|
||||||
s = " ".join(filter(bool, newline_re.split(s)))
|
s = " ".join(filter(None, newline_re.split(s)))
|
||||||
s = " ".join(filter(bool, s.split("\t")))
|
s = " ".join(filter(None, s.split("\t")))
|
||||||
s = " ".join(filter(bool, s.split(" ")))
|
s = " ".join(filter(None, s.split(" ")))
|
||||||
if starts_with_space:
|
if starts_with_space:
|
||||||
s = " " + s
|
s = " " + s
|
||||||
if ends_with_space:
|
if ends_with_space:
|
||||||
@@ -149,10 +151,10 @@ def get_locale_path(cog_folder: Path, extension: str) -> Path:
|
|||||||
return cog_folder / "locales" / "{}.{}".format(get_locale(), extension)
|
return cog_folder / "locales" / "{}.{}".format(get_locale(), extension)
|
||||||
|
|
||||||
|
|
||||||
class Translator:
|
class Translator(Callable[[str], str]):
|
||||||
"""Function to get translated strings at runtime."""
|
"""Function to get translated strings at runtime."""
|
||||||
|
|
||||||
def __init__(self, name, file_location):
|
def __init__(self, name: str, file_location: Union[str, Path, os.PathLike]):
|
||||||
"""
|
"""
|
||||||
Initializes an internationalization object.
|
Initializes an internationalization object.
|
||||||
|
|
||||||
@@ -173,7 +175,7 @@ class Translator:
|
|||||||
|
|
||||||
self.load_translations()
|
self.load_translations()
|
||||||
|
|
||||||
def __call__(self, untranslated: str):
|
def __call__(self, untranslated: str) -> str:
|
||||||
"""Translate the given string.
|
"""Translate the given string.
|
||||||
|
|
||||||
This will look for the string in the translator's :code:`.pot` file,
|
This will look for the string in the translator's :code:`.pot` file,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
# This is basically our old DataIO, except that it's now threadsafe
|
# This is basically our old DataIO and just a base for much more elaborate classes
|
||||||
# and just a base for much more elaborate classes
|
# This still isn't completely threadsafe, (do not use config in threads)
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
log = logging.getLogger("red")
|
log = logging.getLogger("red")
|
||||||
@@ -27,18 +27,50 @@ class JsonIO:
|
|||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
def _save_json(self, data, settings=PRETTY):
|
def _save_json(self, data, settings=PRETTY):
|
||||||
|
"""
|
||||||
|
This fsync stuff here is entirely neccessary.
|
||||||
|
|
||||||
|
On windows, it is not available in entirety.
|
||||||
|
If a windows user ends up with tons of temp files, they should consider hosting on
|
||||||
|
something POSIX compatible, or using the mongo backend instead.
|
||||||
|
|
||||||
|
Most users wont encounter this issue, but with high write volumes,
|
||||||
|
without the fsync on both the temp file, and after the replace on the directory,
|
||||||
|
There's no real durability or atomicity guarantee from the filesystem.
|
||||||
|
|
||||||
|
In depth overview of underlying reasons why this is needed:
|
||||||
|
https://lwn.net/Articles/457667/
|
||||||
|
|
||||||
|
Also see:
|
||||||
|
http://man7.org/linux/man-pages/man2/open.2.html#NOTES (synchronous I/O section)
|
||||||
|
And:
|
||||||
|
https://www.mjmwired.net/kernel/Documentation/filesystems/ext4.txt#310
|
||||||
|
"""
|
||||||
log.debug("Saving file {}".format(self.path))
|
log.debug("Saving file {}".format(self.path))
|
||||||
filename = self.path.stem
|
filename = self.path.stem
|
||||||
tmp_file = "{}-{}.tmp".format(filename, uuid4().fields[0])
|
tmp_file = "{}-{}.tmp".format(filename, uuid4().fields[0])
|
||||||
tmp_path = self.path.parent / tmp_file
|
tmp_path = self.path.parent / tmp_file
|
||||||
with tmp_path.open(encoding="utf-8", mode="w") as f:
|
with tmp_path.open(encoding="utf-8", mode="w") as f:
|
||||||
json.dump(data, f, **settings)
|
json.dump(data, f, **settings)
|
||||||
|
f.flush() # This does get closed on context exit, ...
|
||||||
|
os.fsync(f.fileno()) # but that needs to happen prior to this line
|
||||||
|
|
||||||
tmp_path.replace(self.path)
|
tmp_path.replace(self.path)
|
||||||
|
|
||||||
|
# pylint: disable=E1101
|
||||||
|
try:
|
||||||
|
fd = os.open(self.path.parent, os.O_DIRECTORY)
|
||||||
|
os.fsync(fd)
|
||||||
|
except AttributeError:
|
||||||
|
fd = None
|
||||||
|
finally:
|
||||||
|
if fd is not None:
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
async def _threadsafe_save_json(self, data, settings=PRETTY):
|
async def _threadsafe_save_json(self, data, settings=PRETTY):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
func = functools.partial(self._save_json, data, settings)
|
func = functools.partial(self._save_json, data, settings)
|
||||||
with await self._lock:
|
async with self._lock:
|
||||||
await loop.run_in_executor(None, func)
|
await loop.run_in_executor(None, func)
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
@@ -51,6 +83,5 @@ class JsonIO:
|
|||||||
async def _threadsafe_load_json(self, path):
|
async def _threadsafe_load_json(self, path):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
func = functools.partial(self._load_json, path)
|
func = functools.partial(self._load_json, path)
|
||||||
task = loop.run_in_executor(None, func)
|
async with self._lock:
|
||||||
with await self._lock:
|
return await loop.run_in_executor(None, func)
|
||||||
return await asyncio.wait_for(task)
|
|
||||||
|
|||||||
@@ -1,20 +1,42 @@
|
|||||||
__all__ = ["bounded_gather", "safe_delete", "fuzzy_command_search", "deduplicate_iterables"]
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from asyncio import as_completed, AbstractEventLoop, Semaphore
|
|
||||||
from asyncio.futures import isfuture
|
|
||||||
from itertools import chain
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
import shutil
|
import shutil
|
||||||
from typing import Any, Awaitable, Iterator, List, Optional
|
from asyncio import AbstractEventLoop, as_completed, Semaphore
|
||||||
|
from asyncio.futures import isfuture
|
||||||
|
from itertools import chain
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
AsyncIterator,
|
||||||
|
AsyncIterable,
|
||||||
|
Awaitable,
|
||||||
|
Callable,
|
||||||
|
Iterable,
|
||||||
|
Iterator,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Tuple,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from fuzzywuzzy import fuzz, process
|
||||||
from redbot.core import commands
|
from redbot.core import commands
|
||||||
from fuzzywuzzy import process
|
|
||||||
|
|
||||||
from .chat_formatting import box
|
from .chat_formatting import box
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"bounded_gather",
|
||||||
|
"safe_delete",
|
||||||
|
"fuzzy_command_search",
|
||||||
|
"format_fuzzy_results",
|
||||||
|
"deduplicate_iterables",
|
||||||
|
]
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
|
||||||
# Benchmarked to be the fastest method.
|
# Benchmarked to be the fastest method.
|
||||||
def deduplicate_iterables(*iterables):
|
def deduplicate_iterables(*iterables):
|
||||||
@@ -26,11 +48,11 @@ def deduplicate_iterables(*iterables):
|
|||||||
return list(dict.fromkeys(chain.from_iterable(iterables)))
|
return list(dict.fromkeys(chain.from_iterable(iterables)))
|
||||||
|
|
||||||
|
|
||||||
def fuzzy_filter(record):
|
def _fuzzy_log_filter(record):
|
||||||
return record.funcName != "extractWithoutOrder"
|
return record.funcName != "extractWithoutOrder"
|
||||||
|
|
||||||
|
|
||||||
logging.getLogger().addFilter(fuzzy_filter)
|
logging.getLogger().addFilter(_fuzzy_log_filter)
|
||||||
|
|
||||||
|
|
||||||
def safe_delete(pth: Path):
|
def safe_delete(pth: Path):
|
||||||
@@ -47,59 +69,222 @@ def safe_delete(pth: Path):
|
|||||||
shutil.rmtree(str(pth), ignore_errors=True)
|
shutil.rmtree(str(pth), ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
async def filter_commands(ctx: commands.Context, extracted: list):
|
class AsyncFilter(AsyncIterator[_T], Awaitable[List[_T]]):
|
||||||
return [
|
"""Class returned by `async_filter`. See that function for details.
|
||||||
i
|
|
||||||
for i in extracted
|
We don't recommend instantiating this class directly.
|
||||||
if i[1] >= 90
|
"""
|
||||||
and not i[0].hidden
|
|
||||||
and not any([p.hidden for p in i[0].parents])
|
def __init__(
|
||||||
and await i[0].can_run(ctx)
|
self,
|
||||||
and all([await p.can_run(ctx) for p in i[0].parents])
|
func: Callable[[_T], Union[bool, Awaitable[bool]]],
|
||||||
]
|
iterable: Union[AsyncIterable[_T], Iterable[_T]],
|
||||||
|
) -> None:
|
||||||
|
self.__func: Callable[[_T], Union[bool, Awaitable[bool]]] = func
|
||||||
|
self.__iterable: Union[AsyncIterable[_T], Iterable[_T]] = iterable
|
||||||
|
|
||||||
|
# We assign the generator strategy based on the arguments' types
|
||||||
|
if isinstance(iterable, AsyncIterable):
|
||||||
|
if asyncio.iscoroutinefunction(func):
|
||||||
|
self.__generator_instance = self.__async_generator_async_pred()
|
||||||
|
else:
|
||||||
|
self.__generator_instance = self.__async_generator_sync_pred()
|
||||||
|
elif asyncio.iscoroutinefunction(func):
|
||||||
|
self.__generator_instance = self.__sync_generator_async_pred()
|
||||||
|
else:
|
||||||
|
raise TypeError("Must be either an async predicate, an async iterable, or both.")
|
||||||
|
|
||||||
|
async def __sync_generator_async_pred(self) -> AsyncIterator[_T]:
|
||||||
|
for item in self.__iterable:
|
||||||
|
if await self.__func(item):
|
||||||
|
yield item
|
||||||
|
|
||||||
|
async def __async_generator_sync_pred(self) -> AsyncIterator[_T]:
|
||||||
|
async for item in self.__iterable:
|
||||||
|
if self.__func(item):
|
||||||
|
yield item
|
||||||
|
|
||||||
|
async def __async_generator_async_pred(self) -> AsyncIterator[_T]:
|
||||||
|
async for item in self.__iterable:
|
||||||
|
if await self.__func(item):
|
||||||
|
yield item
|
||||||
|
|
||||||
|
async def __flatten(self) -> List[_T]:
|
||||||
|
return [item async for item in self]
|
||||||
|
|
||||||
|
def __await__(self):
|
||||||
|
# Simply return the generator filled into a list
|
||||||
|
return self.__flatten().__await__()
|
||||||
|
|
||||||
|
def __anext__(self) -> Awaitable[_T]:
|
||||||
|
# This will use the generator strategy set in __init__
|
||||||
|
return self.__generator_instance.__anext__()
|
||||||
|
|
||||||
|
|
||||||
async def fuzzy_command_search(ctx: commands.Context, term: str):
|
def async_filter(
|
||||||
out = []
|
func: Callable[[_T], Union[bool, Awaitable[bool]]],
|
||||||
|
iterable: Union[AsyncIterable[_T], Iterable[_T]],
|
||||||
|
) -> AsyncFilter[_T]:
|
||||||
|
"""Filter an (optionally async) iterable with an (optionally async) predicate.
|
||||||
|
|
||||||
|
At least one of the arguments must be async.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
func : Callable[[T], Union[bool, Awaitable[bool]]]
|
||||||
|
A function or coroutine function which takes one item of ``iterable``
|
||||||
|
as an argument, and returns ``True`` or ``False``.
|
||||||
|
iterable : Union[AsyncIterable[_T], Iterable[_T]]
|
||||||
|
An iterable or async iterable which is to be filtered.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
TypeError
|
||||||
|
If neither of the arguments are async.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
AsyncFilter[T]
|
||||||
|
An object which can either be awaited to yield a list of the filtered
|
||||||
|
items, or can also act as an async iterator to yield items one by one.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return AsyncFilter(func, iterable)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_enumerate(
|
||||||
|
async_iterable: AsyncIterable[_T], start: int = 0
|
||||||
|
) -> AsyncIterator[Tuple[int, _T]]:
|
||||||
|
"""Async iterable version of `enumerate`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
async_iterable : AsyncIterable[T]
|
||||||
|
The iterable to enumerate.
|
||||||
|
start : int
|
||||||
|
The index to start from. Defaults to 0.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
AsyncIterator[Tuple[int, T]]
|
||||||
|
An async iterator of tuples in the form of ``(index, item)``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
async for item in async_iterable:
|
||||||
|
yield start, item
|
||||||
|
start += 1
|
||||||
|
|
||||||
|
|
||||||
|
async def fuzzy_command_search(
|
||||||
|
ctx: commands.Context, term: Optional[str] = None, *, min_score: int = 80
|
||||||
|
) -> Optional[List[commands.Command]]:
|
||||||
|
"""Search for commands which are similar in name to the one invoked.
|
||||||
|
|
||||||
|
Returns a maximum of 5 commands which must all be at least matched
|
||||||
|
greater than ``min_score``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ctx : `commands.Context <redbot.core.commands.Context>`
|
||||||
|
The command invocation context.
|
||||||
|
term : Optional[str]
|
||||||
|
The name of the invoked command. If ``None``, `Context.invoked_with`
|
||||||
|
will be used instead.
|
||||||
|
min_score : int
|
||||||
|
The minimum score for matched commands to reach. Defaults to 80.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Optional[List[`commands.Command <redbot.core.commands.Command>`]]
|
||||||
|
A list of commands which were fuzzily matched with the invoked
|
||||||
|
command.
|
||||||
|
|
||||||
|
"""
|
||||||
if ctx.guild is not None:
|
if ctx.guild is not None:
|
||||||
enabled = await ctx.bot.db.guild(ctx.guild).fuzzy()
|
enabled = await ctx.bot.db.guild(ctx.guild).fuzzy()
|
||||||
else:
|
else:
|
||||||
enabled = await ctx.bot.db.fuzzy()
|
enabled = await ctx.bot.db.fuzzy()
|
||||||
|
|
||||||
if not enabled:
|
if not enabled:
|
||||||
return None
|
return
|
||||||
|
|
||||||
|
if term is None:
|
||||||
|
term = ctx.invoked_with
|
||||||
|
|
||||||
|
# If the term is an alias or CC, we don't want to send a supplementary fuzzy search.
|
||||||
alias_cog = ctx.bot.get_cog("Alias")
|
alias_cog = ctx.bot.get_cog("Alias")
|
||||||
if alias_cog is not None:
|
if alias_cog is not None:
|
||||||
is_alias, alias = await alias_cog.is_alias(ctx.guild, term)
|
is_alias, alias = await alias_cog.is_alias(ctx.guild, term)
|
||||||
|
|
||||||
if is_alias:
|
if is_alias:
|
||||||
return None
|
return
|
||||||
|
|
||||||
customcom_cog = ctx.bot.get_cog("CustomCommands")
|
customcom_cog = ctx.bot.get_cog("CustomCommands")
|
||||||
if customcom_cog is not None:
|
if customcom_cog is not None:
|
||||||
cmd_obj = customcom_cog.commandobj
|
cmd_obj = customcom_cog.commandobj
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ccinfo = await cmd_obj.get(ctx.message, term)
|
await cmd_obj.get(ctx.message, term)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
return None
|
return
|
||||||
|
|
||||||
extracted_cmds = await filter_commands(
|
# Do the scoring. `extracted` is a list of tuples in the form `(command, score)`
|
||||||
ctx, process.extract(term, ctx.bot.walk_commands(), limit=5)
|
extracted = process.extract(term, ctx.bot.walk_commands(), limit=5, scorer=fuzz.QRatio)
|
||||||
)
|
if not extracted:
|
||||||
|
return
|
||||||
|
|
||||||
if not extracted_cmds:
|
# Filter through the fuzzy-matched commands.
|
||||||
return None
|
matched_commands = []
|
||||||
|
for command, score in extracted:
|
||||||
|
if score < min_score:
|
||||||
|
# Since the list is in decreasing order of score, we can exit early.
|
||||||
|
break
|
||||||
|
if await command.can_see(ctx):
|
||||||
|
matched_commands.append(command)
|
||||||
|
|
||||||
for pos, extracted in enumerate(extracted_cmds, 1):
|
return matched_commands
|
||||||
short = " - {}".format(extracted[0].short_doc) if extracted[0].short_doc else ""
|
|
||||||
out.append("{0}. {1.prefix}{2.qualified_name}{3}".format(pos, ctx, extracted[0], short))
|
|
||||||
|
|
||||||
return box("\n".join(out), lang="Perhaps you wanted one of these?")
|
|
||||||
|
async def format_fuzzy_results(
|
||||||
|
ctx: commands.Context,
|
||||||
|
matched_commands: List[commands.Command],
|
||||||
|
*,
|
||||||
|
embed: Optional[bool] = None,
|
||||||
|
) -> Union[str, discord.Embed]:
|
||||||
|
"""Format the result of a fuzzy command search.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ctx : `commands.Context <redbot.core.commands.Context>`
|
||||||
|
The context in which this result is being displayed.
|
||||||
|
matched_commands : List[`commands.Command <redbot.core.commands.Command>`]
|
||||||
|
A list of commands which have been matched by the fuzzy search, sorted
|
||||||
|
in order of decreasing similarity.
|
||||||
|
embed : bool
|
||||||
|
Whether or not the result should be an embed. If set to ``None``, this
|
||||||
|
will default to the result of `ctx.embed_requested`.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Union[str, discord.Embed]
|
||||||
|
The formatted results.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if embed is not False and (embed is True or await ctx.embed_requested()):
|
||||||
|
lines = []
|
||||||
|
for cmd in matched_commands:
|
||||||
|
lines.append(f"**{ctx.clean_prefix}{cmd.qualified_name}** {cmd.short_doc}")
|
||||||
|
return discord.Embed(
|
||||||
|
title="Perhaps you wanted one of these?",
|
||||||
|
colour=await ctx.embed_colour(),
|
||||||
|
description="\n".join(lines),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines = []
|
||||||
|
for cmd in matched_commands:
|
||||||
|
lines.append(f"{ctx.clean_prefix}{cmd.qualified_name} -- {cmd.short_doc}")
|
||||||
|
return "Perhaps you wanted one of these? " + box("\n".join(lines), lang="vhdl")
|
||||||
|
|
||||||
|
|
||||||
async def _sem_wrapper(sem, task):
|
async def _sem_wrapper(sem, task):
|
||||||
@@ -124,9 +309,11 @@ def bounded_gather_iter(
|
|||||||
loop : asyncio.AbstractEventLoop
|
loop : asyncio.AbstractEventLoop
|
||||||
The event loop to use for the semaphore and :meth:`asyncio.gather`.
|
The event loop to use for the semaphore and :meth:`asyncio.gather`.
|
||||||
limit : Optional[`int`]
|
limit : Optional[`int`]
|
||||||
The maximum number of concurrent tasks. Used when no ``semaphore`` is passed.
|
The maximum number of concurrent tasks. Used when no ``semaphore``
|
||||||
|
is passed.
|
||||||
semaphore : Optional[:class:`asyncio.Semaphore`]
|
semaphore : Optional[:class:`asyncio.Semaphore`]
|
||||||
The semaphore to use for bounding tasks. If `None`, create one using ``loop`` and ``limit``.
|
The semaphore to use for bounding tasks. If `None`, create one
|
||||||
|
using ``loop`` and ``limit``.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
@@ -173,9 +360,11 @@ def bounded_gather(
|
|||||||
return_exceptions : bool
|
return_exceptions : bool
|
||||||
If true, gather exceptions in the result list instead of raising.
|
If true, gather exceptions in the result list instead of raising.
|
||||||
limit : Optional[`int`]
|
limit : Optional[`int`]
|
||||||
The maximum number of concurrent tasks. Used when no ``semaphore`` is passed.
|
The maximum number of concurrent tasks. Used when no ``semaphore``
|
||||||
|
is passed.
|
||||||
semaphore : Optional[:class:`asyncio.Semaphore`]
|
semaphore : Optional[:class:`asyncio.Semaphore`]
|
||||||
The semaphore to use for bounding tasks. If `None`, create one using ``loop`` and ``limit``.
|
The semaphore to use for bounding tasks. If `None`, create one
|
||||||
|
using ``loop`` and ``limit``.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import itertools
|
import itertools
|
||||||
from typing import Sequence, Iterator
|
from typing import Sequence, Iterator, List
|
||||||
|
from redbot.core.i18n import Translator
|
||||||
|
|
||||||
|
_ = Translator("UtilsChatFormatting", __file__)
|
||||||
|
|
||||||
|
|
||||||
def error(text: str) -> str:
|
def error(text: str) -> str:
|
||||||
@@ -64,6 +67,7 @@ def bold(text: str) -> str:
|
|||||||
The marked up text.
|
The marked up text.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
text = escape(text, formatting=True)
|
||||||
return "**{}**".format(text)
|
return "**{}**".format(text)
|
||||||
|
|
||||||
|
|
||||||
@@ -101,7 +105,10 @@ def inline(text: str) -> str:
|
|||||||
The marked up text.
|
The marked up text.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return "`{}`".format(text)
|
if "`" in text:
|
||||||
|
return "``{}``".format(text)
|
||||||
|
else:
|
||||||
|
return "`{}`".format(text)
|
||||||
|
|
||||||
|
|
||||||
def italics(text: str) -> str:
|
def italics(text: str) -> str:
|
||||||
@@ -118,6 +125,7 @@ def italics(text: str) -> str:
|
|||||||
The marked up text.
|
The marked up text.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
text = escape(text, formatting=True)
|
||||||
return "*{}*".format(text)
|
return "*{}*".format(text)
|
||||||
|
|
||||||
|
|
||||||
@@ -273,6 +281,7 @@ def strikethrough(text: str) -> str:
|
|||||||
The marked up text.
|
The marked up text.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
text = escape(text, formatting=True)
|
||||||
return "~~{}~~".format(text)
|
return "~~{}~~".format(text)
|
||||||
|
|
||||||
|
|
||||||
@@ -290,6 +299,7 @@ def underline(text: str) -> str:
|
|||||||
The marked up text.
|
The marked up text.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
text = escape(text, formatting=True)
|
||||||
return "__{}__".format(text)
|
return "__{}__".format(text)
|
||||||
|
|
||||||
|
|
||||||
@@ -317,3 +327,33 @@ def escape(text: str, *, mass_mentions: bool = False, formatting: bool = False)
|
|||||||
if formatting:
|
if formatting:
|
||||||
text = text.replace("`", "\\`").replace("*", "\\*").replace("_", "\\_").replace("~", "\\~")
|
text = text.replace("`", "\\`").replace("*", "\\*").replace("_", "\\_").replace("~", "\\~")
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def humanize_list(items: Sequence[str]):
|
||||||
|
"""Get comma-separted list, with the last element joined with *and*.
|
||||||
|
|
||||||
|
This uses an Oxford comma, because without one, items containing
|
||||||
|
the word *and* would make the output difficult to interpret.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
items : Sequence[str]
|
||||||
|
The items of the list to join together.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
.. testsetup::
|
||||||
|
|
||||||
|
from redbot.core.utils.chat_formatting import humanize_list
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
|
||||||
|
>>> humanize_list(['One', 'Two', 'Three'])
|
||||||
|
'One, Two, and Three'
|
||||||
|
>>> humanize_list(['One'])
|
||||||
|
'One'
|
||||||
|
|
||||||
|
"""
|
||||||
|
if len(items) == 1:
|
||||||
|
return items[0]
|
||||||
|
return ", ".join(items[:-1]) + _(", and ") + items[-1]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ __all__ = [
|
|||||||
"filter_invites",
|
"filter_invites",
|
||||||
"filter_mass_mentions",
|
"filter_mass_mentions",
|
||||||
"filter_various_mentions",
|
"filter_various_mentions",
|
||||||
|
"normalize_smartquotes",
|
||||||
]
|
]
|
||||||
|
|
||||||
# regexes
|
# regexes
|
||||||
@@ -19,6 +20,16 @@ MASS_MENTION_RE = re.compile(r"(@)(?=everyone|here)") # This only matches the @
|
|||||||
|
|
||||||
OTHER_MENTION_RE = re.compile(r"(<)(@[!&]?|#)(\d+>)")
|
OTHER_MENTION_RE = re.compile(r"(<)(@[!&]?|#)(\d+>)")
|
||||||
|
|
||||||
|
SMART_QUOTE_REPLACEMENT_DICT = {
|
||||||
|
"\u2018": "'", # Left single quote
|
||||||
|
"\u2019": "'", # Right single quote
|
||||||
|
"\u201C": '"', # Left double quote
|
||||||
|
"\u201D": '"', # Right double quote
|
||||||
|
}
|
||||||
|
|
||||||
|
SMART_QUOTE_REPLACE_RE = re.compile("|".join(SMART_QUOTE_REPLACEMENT_DICT.keys()))
|
||||||
|
|
||||||
|
|
||||||
# convenience wrappers
|
# convenience wrappers
|
||||||
def filter_urls(to_filter: str) -> str:
|
def filter_urls(to_filter: str) -> str:
|
||||||
"""Get a string with URLs sanitized.
|
"""Get a string with URLs sanitized.
|
||||||
@@ -101,3 +112,24 @@ def filter_various_mentions(to_filter: str) -> str:
|
|||||||
The sanitized string.
|
The sanitized string.
|
||||||
"""
|
"""
|
||||||
return OTHER_MENTION_RE.sub(r"\1\\\2\3", to_filter)
|
return OTHER_MENTION_RE.sub(r"\1\\\2\3", to_filter)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_smartquotes(to_normalize: str) -> str:
|
||||||
|
"""
|
||||||
|
Get a string with smart quotes replaced with normal ones
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
to_normalize : str
|
||||||
|
The string to normalize.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The normalized string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def replacement_for(obj):
|
||||||
|
return SMART_QUOTE_REPLACEMENT_DICT.get(obj.group(0), "")
|
||||||
|
|
||||||
|
return SMART_QUOTE_REPLACE_RE.sub(replacement_for, to_normalize)
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
"""
|
# Original source of reaction-based menu idea from
|
||||||
Original source of reaction-based menu idea from
|
# https://github.com/Lunar-Dust/Dusty-Cogs/blob/master/menu/menu.py
|
||||||
https://github.com/Lunar-Dust/Dusty-Cogs/blob/master/menu/menu.py
|
#
|
||||||
|
# Ported to Red V3 by Palm\_\_ (https://github.com/palmtree5)
|
||||||
Ported to Red V3 by Palm\_\_ (https://github.com/palmtree5)
|
|
||||||
"""
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
from typing import Union, Iterable
|
from typing import Union, Iterable, Optional
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from redbot.core import commands
|
from .. import commands
|
||||||
|
from .predicates import ReactionPredicate
|
||||||
|
|
||||||
_ReactableEmoji = Union[str, discord.Emoji]
|
_ReactableEmoji = Union[str, discord.Emoji]
|
||||||
|
|
||||||
@@ -71,18 +70,20 @@ async def menu(
|
|||||||
else:
|
else:
|
||||||
message = await ctx.send(current_page)
|
message = await ctx.send(current_page)
|
||||||
# Don't wait for reactions to be added (GH-1797)
|
# Don't wait for reactions to be added (GH-1797)
|
||||||
ctx.bot.loop.create_task(_add_menu_reactions(message, controls.keys()))
|
# noinspection PyAsyncCall
|
||||||
|
start_adding_reactions(message, controls.keys(), ctx.bot.loop)
|
||||||
else:
|
else:
|
||||||
if isinstance(current_page, discord.Embed):
|
if isinstance(current_page, discord.Embed):
|
||||||
await message.edit(embed=current_page)
|
await message.edit(embed=current_page)
|
||||||
else:
|
else:
|
||||||
await message.edit(content=current_page)
|
await message.edit(content=current_page)
|
||||||
|
|
||||||
def react_check(r, u):
|
|
||||||
return u == ctx.author and r.message.id == message.id and str(r.emoji) in controls.keys()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
react, user = await ctx.bot.wait_for("reaction_add", check=react_check, timeout=timeout)
|
react, user = await ctx.bot.wait_for(
|
||||||
|
"reaction_add",
|
||||||
|
check=ReactionPredicate.with_emojis(tuple(controls.keys()), message, ctx.author),
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
try:
|
try:
|
||||||
await message.clear_reactions()
|
await message.clear_reactions()
|
||||||
@@ -103,7 +104,7 @@ async def next_page(
|
|||||||
timeout: float,
|
timeout: float,
|
||||||
emoji: str,
|
emoji: str,
|
||||||
):
|
):
|
||||||
perms = message.channel.permissions_for(ctx.guild.me)
|
perms = message.channel.permissions_for(ctx.me)
|
||||||
if perms.manage_messages: # Can manage messages, so remove react
|
if perms.manage_messages: # Can manage messages, so remove react
|
||||||
try:
|
try:
|
||||||
await message.remove_reaction(emoji, ctx.author)
|
await message.remove_reaction(emoji, ctx.author)
|
||||||
@@ -125,17 +126,17 @@ async def prev_page(
|
|||||||
timeout: float,
|
timeout: float,
|
||||||
emoji: str,
|
emoji: str,
|
||||||
):
|
):
|
||||||
perms = message.channel.permissions_for(ctx.guild.me)
|
perms = message.channel.permissions_for(ctx.me)
|
||||||
if perms.manage_messages: # Can manage messages, so remove react
|
if perms.manage_messages: # Can manage messages, so remove react
|
||||||
try:
|
try:
|
||||||
await message.remove_reaction(emoji, ctx.author)
|
await message.remove_reaction(emoji, ctx.author)
|
||||||
except discord.NotFound:
|
except discord.NotFound:
|
||||||
pass
|
pass
|
||||||
if page == 0:
|
if page == 0:
|
||||||
next_page = len(pages) - 1 # Loop around to the last item
|
page = len(pages) - 1 # Loop around to the last item
|
||||||
else:
|
else:
|
||||||
next_page = page - 1
|
page = page - 1
|
||||||
return await menu(ctx, pages, controls, message=message, page=next_page, timeout=timeout)
|
return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
async def close_menu(
|
async def close_menu(
|
||||||
@@ -152,12 +153,51 @@ async def close_menu(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def _add_menu_reactions(message: discord.Message, emojis: Iterable[_ReactableEmoji]):
|
def start_adding_reactions(
|
||||||
"""Add the reactions"""
|
message: discord.Message,
|
||||||
# The task should exit silently if the message is deleted
|
emojis: Iterable[_ReactableEmoji],
|
||||||
with contextlib.suppress(discord.NotFound):
|
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||||
for emoji in emojis:
|
) -> asyncio.Task:
|
||||||
await message.add_reaction(emoji)
|
"""Start adding reactions to a message.
|
||||||
|
|
||||||
|
This is a non-blocking operation - calling this will schedule the
|
||||||
|
reactions being added, but will the calling code will continue to
|
||||||
|
execute asynchronously. There is no need to await this function.
|
||||||
|
|
||||||
|
This is particularly useful if you wish to start waiting for a
|
||||||
|
reaction whilst the reactions are still being added - in fact,
|
||||||
|
this is exactly what `menu` uses to do that.
|
||||||
|
|
||||||
|
This spawns a `asyncio.Task` object and schedules it on ``loop``.
|
||||||
|
If ``loop`` omitted, the loop will be retreived with
|
||||||
|
`asyncio.get_event_loop`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
message: discord.Message
|
||||||
|
The message to add reactions to.
|
||||||
|
emojis : Iterable[Union[str, discord.Emoji]]
|
||||||
|
The emojis to react to the message with.
|
||||||
|
loop : Optional[asyncio.AbstractEventLoop]
|
||||||
|
The event loop.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
asyncio.Task
|
||||||
|
The task for the coroutine adding the reactions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def task():
|
||||||
|
# The task should exit silently if the message is deleted
|
||||||
|
with contextlib.suppress(discord.NotFound):
|
||||||
|
for emoji in emojis:
|
||||||
|
await message.add_reaction(emoji)
|
||||||
|
|
||||||
|
if loop is None:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
return loop.create_task(task())
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONTROLS = {"⬅": prev_page, "❌": close_menu, "➡": next_page}
|
DEFAULT_CONTROLS = {"⬅": prev_page, "❌": close_menu, "➡": next_page}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import List, Iterable, Union
|
from typing import List, Iterable, Union, TYPE_CHECKING, Dict
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from redbot.core import Config
|
if TYPE_CHECKING:
|
||||||
from redbot.core.bot import Red
|
from .. import Config
|
||||||
|
from ..bot import Red
|
||||||
|
from ..commands import Context
|
||||||
|
|
||||||
|
|
||||||
async def mass_purge(messages: List[discord.Message], channel: discord.TextChannel):
|
async def mass_purge(messages: List[discord.Message], channel: discord.TextChannel):
|
||||||
@@ -87,7 +89,7 @@ def get_audit_reason(author: discord.Member, reason: str = None):
|
|||||||
|
|
||||||
|
|
||||||
async def is_allowed_by_hierarchy(
|
async def is_allowed_by_hierarchy(
|
||||||
bot: Red, settings: Config, guild: discord.Guild, mod: discord.Member, user: discord.Member
|
bot: "Red", settings: "Config", guild: discord.Guild, mod: discord.Member, user: discord.Member
|
||||||
):
|
):
|
||||||
if not await settings.guild(guild).respect_hierarchy():
|
if not await settings.guild(guild).respect_hierarchy():
|
||||||
return True
|
return True
|
||||||
@@ -95,7 +97,9 @@ async def is_allowed_by_hierarchy(
|
|||||||
return mod.top_role.position > user.top_role.position or is_special
|
return mod.top_role.position > user.top_role.position or is_special
|
||||||
|
|
||||||
|
|
||||||
async def is_mod_or_superior(bot: Red, obj: Union[discord.Message, discord.Member, discord.Role]):
|
async def is_mod_or_superior(
|
||||||
|
bot: "Red", obj: Union[discord.Message, discord.Member, discord.Role]
|
||||||
|
):
|
||||||
"""Check if an object has mod or superior permissions.
|
"""Check if an object has mod or superior permissions.
|
||||||
|
|
||||||
If a message is passed, its author's permissions are checked. If a role is
|
If a message is passed, its author's permissions are checked. If a role is
|
||||||
@@ -179,7 +183,7 @@ def strfdelta(delta: timedelta):
|
|||||||
|
|
||||||
|
|
||||||
async def is_admin_or_superior(
|
async def is_admin_or_superior(
|
||||||
bot: Red, obj: Union[discord.Message, discord.Member, discord.Role]
|
bot: "Red", obj: Union[discord.Message, discord.Member, discord.Role]
|
||||||
):
|
):
|
||||||
"""Same as `is_mod_or_superior` except for admin permissions.
|
"""Same as `is_mod_or_superior` except for admin permissions.
|
||||||
|
|
||||||
@@ -225,3 +229,36 @@ async def is_admin_or_superior(
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def check_permissions(ctx: "Context", perms: Dict[str, bool]) -> bool:
|
||||||
|
"""Check if the author has required permissions.
|
||||||
|
|
||||||
|
This will always return ``True`` if the author is a bot owner, or
|
||||||
|
has the ``administrator`` permission. If ``perms`` is empty, this
|
||||||
|
will only check if the user is a bot owner.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ctx : Context
|
||||||
|
The command invokation context to check.
|
||||||
|
perms : Dict[str, bool]
|
||||||
|
A dictionary mapping permissions to their required states.
|
||||||
|
Valid permission names are those listed as properties of
|
||||||
|
the `discord.Permissions` class.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
``True`` if the author has the required permissions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if await ctx.bot.is_owner(ctx.author):
|
||||||
|
return True
|
||||||
|
elif not perms:
|
||||||
|
return False
|
||||||
|
resolved = ctx.channel.permissions_for(ctx.author)
|
||||||
|
|
||||||
|
return resolved.administrator or all(
|
||||||
|
getattr(resolved, name, None) == value for name, value in perms.items()
|
||||||
|
)
|
||||||
|
|||||||
1015
redbot/core/utils/predicates.py
Normal file
1015
redbot/core/utils/predicates.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ from redbot.core.utils.chat_formatting import pagify
|
|||||||
import io
|
import io
|
||||||
import sys
|
import sys
|
||||||
import weakref
|
import weakref
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
from .common_filters import filter_mass_mentions
|
from .common_filters import filter_mass_mentions
|
||||||
|
|
||||||
_instances = weakref.WeakValueDictionary({})
|
_instances = weakref.WeakValueDictionary({})
|
||||||
@@ -86,7 +86,11 @@ class Tunnel(metaclass=TunnelMeta):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def message_forwarder(
|
async def message_forwarder(
|
||||||
*, destination: discord.abc.Messageable, content: str = None, embed=None, files=[]
|
*,
|
||||||
|
destination: discord.abc.Messageable,
|
||||||
|
content: str = None,
|
||||||
|
embed=None,
|
||||||
|
files: Optional[List[discord.File]] = None
|
||||||
) -> List[discord.Message]:
|
) -> List[discord.Message]:
|
||||||
"""
|
"""
|
||||||
This does the actual sending, use this instead of a full tunnel
|
This does the actual sending, use this instead of a full tunnel
|
||||||
@@ -95,19 +99,19 @@ class Tunnel(metaclass=TunnelMeta):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
destination: `discord.abc.Messageable`
|
destination: discord.abc.Messageable
|
||||||
Where to send
|
Where to send
|
||||||
content: `str`
|
content: str
|
||||||
The message content
|
The message content
|
||||||
embed: `discord.Embed`
|
embed: discord.Embed
|
||||||
The embed to send
|
The embed to send
|
||||||
files: `list` of `discord.File`
|
files: Optional[List[discord.File]]
|
||||||
A list of files to send.
|
A list of files to send.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
list of `discord.Message`
|
List[discord.Message]
|
||||||
The `discord.Message`\ (s) sent as a result
|
The messages sent as a result.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
@@ -117,7 +121,6 @@ class Tunnel(metaclass=TunnelMeta):
|
|||||||
see `discord.abc.Messageable.send`
|
see `discord.abc.Messageable.send`
|
||||||
"""
|
"""
|
||||||
rets = []
|
rets = []
|
||||||
files = files if files else None
|
|
||||||
if content:
|
if content:
|
||||||
for page in pagify(content):
|
for page in pagify(content):
|
||||||
rets.append(await destination.send(page, files=files, embed=embed))
|
rets.append(await destination.send(page, files=files, embed=embed))
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ from redbot.core.cli import confirm
|
|||||||
if sys.platform == "linux":
|
if sys.platform == "linux":
|
||||||
import distro
|
import distro
|
||||||
|
|
||||||
PYTHON_OK = sys.version_info >= (3, 6, 2)
|
|
||||||
INTERACTIVE_MODE = not len(sys.argv) > 1 # CLI flags = non-interactive
|
INTERACTIVE_MODE = not len(sys.argv) > 1 # CLI flags = non-interactive
|
||||||
|
|
||||||
INTRO = "==========================\nRed Discord Bot - Launcher\n==========================\n"
|
INTRO = "==========================\nRed Discord Bot - Launcher\n==========================\n"
|
||||||
@@ -33,6 +32,14 @@ INTRO = "==========================\nRed Discord Bot - Launcher\n===============
|
|||||||
IS_WINDOWS = os.name == "nt"
|
IS_WINDOWS = os.name == "nt"
|
||||||
IS_MAC = sys.platform == "darwin"
|
IS_MAC = sys.platform == "darwin"
|
||||||
|
|
||||||
|
if IS_WINDOWS:
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
PYTHON_OK = sys.version_info >= MIN_PYTHON_VERSION
|
||||||
|
|
||||||
|
|
||||||
def is_venv():
|
def is_venv():
|
||||||
"""Return True if the process is in a venv or in a virtualenv."""
|
"""Return True if the process is in a venv or in a virtualenv."""
|
||||||
@@ -409,14 +416,14 @@ def main_menu():
|
|||||||
choice = user_choice()
|
choice = user_choice()
|
||||||
if choice == "1":
|
if choice == "1":
|
||||||
instance = instance_menu()
|
instance = instance_menu()
|
||||||
cli_flags = cli_flag_getter()
|
|
||||||
if instance:
|
if instance:
|
||||||
|
cli_flags = cli_flag_getter()
|
||||||
run_red(instance, autorestart=True, cliflags=cli_flags)
|
run_red(instance, autorestart=True, cliflags=cli_flags)
|
||||||
wait()
|
wait()
|
||||||
elif choice == "2":
|
elif choice == "2":
|
||||||
instance = instance_menu()
|
instance = instance_menu()
|
||||||
cli_flags = cli_flag_getter()
|
|
||||||
if instance:
|
if instance:
|
||||||
|
cli_flags = cli_flag_getter()
|
||||||
run_red(instance, autorestart=False, cliflags=cli_flags)
|
run_red(instance, autorestart=False, cliflags=cli_flags)
|
||||||
wait()
|
wait()
|
||||||
elif choice == "3":
|
elif choice == "3":
|
||||||
@@ -461,9 +468,11 @@ def main_menu():
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
if not PYTHON_OK:
|
if not PYTHON_OK:
|
||||||
raise RuntimeError(
|
print(
|
||||||
"Red requires Python 3.6.2 or greater. Please install the correct version!"
|
f"Python {'.'.join(map(str, MIN_PYTHON_VERSION))} is required to run Red, but you "
|
||||||
|
f"have {sys.version}! Please update Python."
|
||||||
)
|
)
|
||||||
|
sys.exit(1)
|
||||||
if args.debuginfo: # Check first since the function triggers an exit
|
if args.debuginfo: # Check first since the function triggers an exit
|
||||||
debug_info()
|
debug_info()
|
||||||
|
|
||||||
|
|||||||
11
redbot/pytest/permissions.py
Normal file
11
redbot/pytest/permissions.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from redbot.cogs.permissions import Permissions
|
||||||
|
from redbot.core import Config
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def permissions(config, monkeypatch, red):
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
|
||||||
|
return Permissions(red)
|
||||||
112
setup.py
112
setup.py
@@ -5,9 +5,9 @@ import tempfile
|
|||||||
from distutils.errors import CCompilerError, DistutilsPlatformError
|
from distutils.errors import CCompilerError, DistutilsPlatformError
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
requirements = [
|
install_requires = [
|
||||||
"aiohttp-json-rpc==0.11.1",
|
"aiohttp-json-rpc==0.11.2",
|
||||||
"aiohttp==3.3.2",
|
"aiohttp==3.4.4",
|
||||||
"appdirs==1.4.3",
|
"appdirs==1.4.3",
|
||||||
"async-timeout==3.0.0",
|
"async-timeout==3.0.0",
|
||||||
"attrs==18.2.0",
|
"attrs==18.2.0",
|
||||||
@@ -18,15 +18,57 @@ requirements = [
|
|||||||
"fuzzywuzzy==0.17.0",
|
"fuzzywuzzy==0.17.0",
|
||||||
"idna-ssl==1.1.0",
|
"idna-ssl==1.1.0",
|
||||||
"idna==2.7",
|
"idna==2.7",
|
||||||
"multidict==4.4.0",
|
"multidict==4.4.2",
|
||||||
"python-levenshtein==0.12.0",
|
"python-levenshtein==0.12.0",
|
||||||
"pyyaml==3.13",
|
"pyyaml==3.13",
|
||||||
"raven==6.9.0",
|
"raven==6.9.0",
|
||||||
"raven-aiohttp==0.7.0",
|
"raven-aiohttp==0.7.0",
|
||||||
|
"schema==0.6.8",
|
||||||
"websockets==6.0",
|
"websockets==6.0",
|
||||||
"yarl==1.2.6",
|
"yarl==1.2.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
extras_require = {
|
||||||
|
"test": [
|
||||||
|
"atomicwrites==1.2.1",
|
||||||
|
"more-itertools==4.3.0",
|
||||||
|
"pluggy==0.7.1",
|
||||||
|
"py==1.6.0",
|
||||||
|
"pytest==3.8.2",
|
||||||
|
"pytest-asyncio==0.9.0",
|
||||||
|
"six==1.11.0",
|
||||||
|
],
|
||||||
|
"mongo": ["motor==2.0.0", "pymongo==3.7.1", "dnspython==1.15.0"],
|
||||||
|
"docs": [
|
||||||
|
"alabaster==0.7.11",
|
||||||
|
"babel==2.6.0",
|
||||||
|
"certifi==2018.8.24",
|
||||||
|
"docutils==0.14",
|
||||||
|
"imagesize==1.1.0",
|
||||||
|
"Jinja2==2.10",
|
||||||
|
"MarkupSafe==1.0",
|
||||||
|
"packaging==18.0",
|
||||||
|
"pyparsing==2.2.2",
|
||||||
|
"Pygments==2.2.0",
|
||||||
|
"pytz==2018.5",
|
||||||
|
"requests==2.19.1",
|
||||||
|
"urllib3==1.23",
|
||||||
|
"six==1.11.0",
|
||||||
|
"snowballstemmer==1.2.1",
|
||||||
|
"sphinx==1.7.9",
|
||||||
|
"sphinx_rtd_theme==0.4.1",
|
||||||
|
"sphinxcontrib-asyncio==0.2.0",
|
||||||
|
"sphinxcontrib-websupport==1.1.0",
|
||||||
|
],
|
||||||
|
"voice": ["red-lavalink==0.1.2"],
|
||||||
|
"style": ["black==18.9b0", "click==7.0", "toml==0.9.6"],
|
||||||
|
}
|
||||||
|
|
||||||
|
python_requires = ">=3.6.2,<3.8"
|
||||||
|
if os.name == "nt":
|
||||||
|
# Due to issues with ProactorEventLoop prior to 3.6.6 (bpo-26819)
|
||||||
|
python_requires = ">=3.6.6,<3.8"
|
||||||
|
|
||||||
|
|
||||||
def get_dependency_links():
|
def get_dependency_links():
|
||||||
with open("dependency_links.txt") as file:
|
with open("dependency_links.txt") as file:
|
||||||
@@ -37,13 +79,12 @@ def check_compiler_available():
|
|||||||
m = ccompiler.new_compiler()
|
m = ccompiler.new_compiler()
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tdir:
|
with tempfile.TemporaryDirectory() as tdir:
|
||||||
with tempfile.NamedTemporaryFile(prefix="dummy", suffix=".c", dir=tdir) as tfile:
|
with open(os.path.join(tdir, "dummy.c"), "w") as tfile:
|
||||||
tfile.write(b"int main(int argc, char** argv) {return 0;}")
|
tfile.write("int main(int argc, char** argv) {return 0;}")
|
||||||
tfile.seek(0)
|
try:
|
||||||
try:
|
m.compile([tfile.name], output_dir=tdir)
|
||||||
m.compile([tfile.name], output_dir=tdir)
|
except (CCompilerError, DistutilsPlatformError):
|
||||||
except (CCompilerError, DistutilsPlatformError):
|
return False
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -57,12 +98,14 @@ def get_version():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if not check_compiler_available():
|
if not check_compiler_available():
|
||||||
requirements.remove(
|
install_requires.remove(
|
||||||
next(r for r in requirements if r.lower().startswith("python-levenshtein"))
|
next(r for r in install_requires if r.lower().startswith("python-levenshtein"))
|
||||||
)
|
)
|
||||||
|
|
||||||
if "READTHEDOCS" in os.environ:
|
if "READTHEDOCS" in os.environ:
|
||||||
requirements.remove(next(r for r in requirements if r.lower().startswith("discord.py")))
|
install_requires.remove(
|
||||||
|
next(r for r in install_requires if r.lower().startswith("discord.py"))
|
||||||
|
)
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="Red-DiscordBot",
|
name="Red-DiscordBot",
|
||||||
@@ -94,43 +137,8 @@ if __name__ == "__main__":
|
|||||||
],
|
],
|
||||||
"pytest11": ["red-discordbot = redbot.pytest"],
|
"pytest11": ["red-discordbot = redbot.pytest"],
|
||||||
},
|
},
|
||||||
python_requires=">=3.6.2,<3.8",
|
python_requires=python_requires,
|
||||||
install_requires=requirements,
|
install_requires=install_requires,
|
||||||
dependency_links=get_dependency_links(),
|
dependency_links=get_dependency_links(),
|
||||||
extras_require={
|
extras_require=extras_require,
|
||||||
"test": [
|
|
||||||
"atomicwrites==1.2.1",
|
|
||||||
"more-itertools==4.3.0",
|
|
||||||
"pluggy==0.7.1",
|
|
||||||
"py==1.6.0",
|
|
||||||
"pytest==3.7.4",
|
|
||||||
"pytest-asyncio==0.9.0",
|
|
||||||
"six==1.11.0",
|
|
||||||
],
|
|
||||||
"mongo": ["motor==2.0.0", "pymongo==3.7.1"],
|
|
||||||
"docs": [
|
|
||||||
"alabaster==0.7.11",
|
|
||||||
"babel==2.6.0",
|
|
||||||
"certifi==2018.8.24",
|
|
||||||
"docutils==0.14",
|
|
||||||
"imagesize==1.1.0",
|
|
||||||
"Jinja2==2.10",
|
|
||||||
"MarkupSafe==1.0",
|
|
||||||
"packaging==17.1",
|
|
||||||
"pyparsing==2.2.0",
|
|
||||||
"six==1.11.0",
|
|
||||||
"Pygments==2.2.0",
|
|
||||||
"pytz==2018.5",
|
|
||||||
"requests==2.19.1",
|
|
||||||
"urllib3==1.23",
|
|
||||||
"six==1.11.0",
|
|
||||||
"snowballstemmer==1.2.1",
|
|
||||||
"sphinx==1.7.8",
|
|
||||||
"sphinx_rtd_theme==0.4.1",
|
|
||||||
"sphinxcontrib-asyncio==0.2.0",
|
|
||||||
"sphinxcontrib-websupport==1.1.0",
|
|
||||||
],
|
|
||||||
"voice": ["red-lavalink==0.1.2"],
|
|
||||||
"style": ["black==18.6b4", "click==6.7", "toml==0.9.4"],
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,20 +28,6 @@ def test_existing_git_repo(tmpdir):
|
|||||||
assert exists is True
|
assert exists is True
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_clone_repo(repo_norun, capsys):
|
|
||||||
await repo_norun.clone()
|
|
||||||
|
|
||||||
clone_cmd, _ = capsys.readouterr()
|
|
||||||
clone_cmd = clone_cmd.strip("[']\n").split("', '")
|
|
||||||
assert clone_cmd[0] == "git"
|
|
||||||
assert clone_cmd[1] == "clone"
|
|
||||||
assert clone_cmd[2] == "-b"
|
|
||||||
assert clone_cmd[3] == "rewrite_cogs"
|
|
||||||
assert clone_cmd[4] == repo_norun.url
|
|
||||||
assert ("repos", "squid") == pathlib.Path(clone_cmd[5]).parts[-2:]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_repo(monkeypatch, repo_manager):
|
async def test_add_repo(monkeypatch, repo_manager):
|
||||||
monkeypatch.setattr("redbot.cogs.downloader.repo_manager.Repo._run", fake_run_noprint)
|
monkeypatch.setattr("redbot.cogs.downloader.repo_manager.Repo._run", fake_run_noprint)
|
||||||
@@ -94,3 +80,43 @@ async def test_existing_repo(repo_manager):
|
|||||||
await repo_manager.add_repo("http://test.com", "test")
|
await repo_manager.add_repo("http://test.com", "test")
|
||||||
|
|
||||||
repo_manager.does_repo_exist.assert_called_once_with("test")
|
repo_manager.does_repo_exist.assert_called_once_with("test")
|
||||||
|
|
||||||
|
|
||||||
|
def test_tree_url_parse(repo_manager):
|
||||||
|
cases = [
|
||||||
|
{
|
||||||
|
"input": ("https://github.com/Tobotimus/Tobo-Cogs", None),
|
||||||
|
"expected": ("https://github.com/Tobotimus/Tobo-Cogs", None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": ("https://github.com/Tobotimus/Tobo-Cogs", "V3"),
|
||||||
|
"expected": ("https://github.com/Tobotimus/Tobo-Cogs", "V3"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": ("https://github.com/Tobotimus/Tobo-Cogs/tree/V3", None),
|
||||||
|
"expected": ("https://github.com/Tobotimus/Tobo-Cogs", "V3"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": ("https://github.com/Tobotimus/Tobo-Cogs/tree/V3", "V4"),
|
||||||
|
"expected": ("https://github.com/Tobotimus/Tobo-Cogs", "V4"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for test_case in cases:
|
||||||
|
assert test_case["expected"] == repo_manager._parse_url(*test_case["input"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_tree_url_non_github(repo_manager):
|
||||||
|
cases = [
|
||||||
|
{
|
||||||
|
"input": ("https://gitlab.com/Tobotimus/Tobo-Cogs", None),
|
||||||
|
"expected": ("https://gitlab.com/Tobotimus/Tobo-Cogs", None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": ("https://my.usgs.gov/bitbucket/scm/Tobotimus/Tobo-Cogs", "V3"),
|
||||||
|
"expected": ("https://my.usgs.gov/bitbucket/scm/Tobotimus/Tobo-Cogs", "V3"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for test_case in cases:
|
||||||
|
assert test_case["expected"] == repo_manager._parse_url(*test_case["input"])
|
||||||
|
|||||||
66
tests/cogs/test_permissions.py
Normal file
66
tests/cogs/test_permissions.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from redbot.cogs.permissions.permissions import Permissions, GLOBAL
|
||||||
|
|
||||||
|
|
||||||
|
def test_schema_update():
|
||||||
|
old = {
|
||||||
|
GLOBAL: {
|
||||||
|
"owner_models": {
|
||||||
|
"cogs": {
|
||||||
|
"Admin": {"allow": [78631113035100160], "deny": [96733288462286848]},
|
||||||
|
"Audio": {"allow": [133049272517001216], "default": "deny"},
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"cleanup bot": {"allow": [78631113035100160], "default": "deny"},
|
||||||
|
"ping": {
|
||||||
|
"allow": [96733288462286848],
|
||||||
|
"deny": [96733288462286848],
|
||||||
|
"default": "allow",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
43733288462286848: {
|
||||||
|
"owner_models": {
|
||||||
|
"cogs": {
|
||||||
|
"Admin": {
|
||||||
|
"allow": [24231113035100160],
|
||||||
|
"deny": [35533288462286848, 24231113035100160],
|
||||||
|
},
|
||||||
|
"General": {"allow": [133049272517001216], "default": "deny"},
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"cleanup bot": {"allow": [17831113035100160], "default": "allow"},
|
||||||
|
"set adminrole": {
|
||||||
|
"allow": [87733288462286848],
|
||||||
|
"deny": [95433288462286848],
|
||||||
|
"default": "allow",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
new = Permissions._get_updated_schema(old)
|
||||||
|
assert new == (
|
||||||
|
{
|
||||||
|
"Admin": {
|
||||||
|
GLOBAL: {78631113035100160: True, 96733288462286848: False},
|
||||||
|
43733288462286848: {24231113035100160: True, 35533288462286848: False},
|
||||||
|
},
|
||||||
|
"Audio": {GLOBAL: {133049272517001216: True, "default": False}},
|
||||||
|
"General": {43733288462286848: {133049272517001216: True, "default": False}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cleanup bot": {
|
||||||
|
GLOBAL: {78631113035100160: True, "default": False},
|
||||||
|
43733288462286848: {17831113035100160: True, "default": True},
|
||||||
|
},
|
||||||
|
"ping": {GLOBAL: {96733288462286848: True, "default": True}},
|
||||||
|
"set adminrole": {
|
||||||
|
43733288462286848: {
|
||||||
|
87733288462286848: True,
|
||||||
|
95433288462286848: False,
|
||||||
|
"default": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -224,6 +224,15 @@ async def test_set_dynamic_attr(config):
|
|||||||
assert await config.foobar() is True
|
assert await config.foobar() is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_clear_dynamic_attr(config):
|
||||||
|
await config.foo.set(True)
|
||||||
|
await config.clear_raw("foo")
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
await config.get_raw("foo")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_dynamic_attr(config):
|
async def test_get_dynamic_attr(config):
|
||||||
assert await config.get_raw("foobaz", default=True) is True
|
assert await config.get_raw("foobaz", default=True) is True
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import asyncio
|
|||||||
import pytest
|
import pytest
|
||||||
import random
|
import random
|
||||||
import textwrap
|
import textwrap
|
||||||
import warnings
|
|
||||||
from redbot.core.utils import (
|
from redbot.core.utils import (
|
||||||
chat_formatting,
|
chat_formatting,
|
||||||
bounded_gather,
|
bounded_gather,
|
||||||
bounded_gather_iter,
|
bounded_gather_iter,
|
||||||
deduplicate_iterables,
|
deduplicate_iterables,
|
||||||
|
common_filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ async def test_bounded_gather():
|
|||||||
if isinstance(result, RuntimeError):
|
if isinstance(result, RuntimeError):
|
||||||
num_failed += 1
|
num_failed += 1
|
||||||
else:
|
else:
|
||||||
assert result == i # verify original orde
|
assert result == i # verify_permissions original orde
|
||||||
assert 0 <= result < num_tasks
|
assert 0 <= result < num_tasks
|
||||||
|
|
||||||
assert 0 < status[1] <= num_concurrent
|
assert 0 < status[1] <= num_concurrent
|
||||||
@@ -191,3 +191,8 @@ async def test_bounded_gather_iter_cancel():
|
|||||||
assert 0 < status[1] <= num_concurrent
|
assert 0 < status[1] <= num_concurrent
|
||||||
assert quit_on <= status[2] <= quit_on + num_concurrent
|
assert quit_on <= status[2] <= quit_on + num_concurrent
|
||||||
assert num_failed <= num_fail
|
assert num_failed <= num_fail
|
||||||
|
|
||||||
|
|
||||||
|
def test_normalize_smartquotes():
|
||||||
|
assert common_filters.normalize_smartquotes("Should\u2018 normalize") == "Should' normalize"
|
||||||
|
assert common_filters.normalize_smartquotes("Same String") == "Same String"
|
||||||
|
|||||||
5
tox.ini
5
tox.ini
@@ -29,8 +29,9 @@ whitelist_externals =
|
|||||||
basepython = python3.6
|
basepython = python3.6
|
||||||
extras = docs, mongo
|
extras = docs, mongo
|
||||||
commands =
|
commands =
|
||||||
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" -W -bhtml
|
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out/html" -W -bhtml
|
||||||
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" -W -blinkcheck
|
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out/linkcheck" -W -blinkcheck
|
||||||
|
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out/doctest" -W -bdoctest
|
||||||
|
|
||||||
[testenv:style]
|
[testenv:style]
|
||||||
description = Stylecheck the code with black to see if anything needs changes.
|
description = Stylecheck the code with black to see if anything needs changes.
|
||||||
|
|||||||
Reference in New Issue
Block a user