Compare commits

..

153 Commits

Author SHA1 Message Date
aikaterna
3a968d707f Fix Travis deployment attempt 2 (#2061) 2018-08-27 10:24:03 +10:00
aikaterna
b07c44c8b4 Fix travis deployment (#2060) 2018-08-27 10:09:02 +10:00
aikaterna
43b0a58649 [Audio] Fix for embed color (no, COLOUR) on notify messages (#2059) 2018-08-27 09:44:19 +10:00
Toby Harradine
f258e93cf7 Bump version to 3.0.0b20 (#2050)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-08-27 09:28:37 +10:00
Toby Harradine
93138b04cb [Streams] Set YouTube channel name when added by ID (#2047)
* [Streams] Set YouTube channel name when added by ID

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>

* Move unset token raise to correct place

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>

* Correct logic in get_stream

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>

* Fetch name explicitly instead

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-08-26 23:53:47 +10:00
Toby Harradine
0cf54ec9c2 [Audio] Do less strict matching for java version (#2035)
* [Audio] Do less strict matching for java version

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>

* [Audio] Fix java version bounds to account for Oracle's bullshit

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-08-26 23:49:27 +10:00
Toby Harradine
ce031cf7bd [Docs] Add virtualenv guide and compress install guides (#2029)
* [Docs] Add virtualenv guide and compress install guides

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>

* [Docs] Better cross-referencing

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>

* Fix pyenv-installer link

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>

* Use sudo -e instead of sudo nano

* Add note about launcher for linux/mac

* Include launcher notes in Windows guide

* Add missing colon
2018-08-26 23:44:56 +10:00
Toby Harradine
e6495bc7c0 [Trivia] Move Trivia lists back home (#2028)
* [Trivia] Move trivia lists back home

Removes red-trivia as a dependency.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>

* Include package data in distribution

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>

* Add test from red-trivia repo, and fix package data setup

* The distribution will now include all files under any data/ sub-directory of a package, as well as all *.po files under any locales/ sub-directory (as it should have been before).

* MANIFEST.in has been simplified to comply with these changes and redbot/cogs/audio/application.yml has been moved to the data/ sub-directory to maintain consistency in how we declare package data.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-08-26 23:39:46 +10:00
Toby Harradine
1b196bf0fb [i18n] Use redgettext over pygettext (#2023)
* [i18n] Use redgettext over pygettext

* Clear out autogenerated `messages.pot` files

* Remove redundant `regen_messages.py` files

* Refactor `generate_strings.py` to use redgettext

* Install redgettext in Travis Crowdin job

* Clean up some problematic usages of gettext function

* Reformat

* Replace generate_strings.py with Makefile argument

* Update to redgettext 2.1, use exclusion pattern

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-08-26 23:35:42 +10:00
Toby Harradine
dbed24aaca [Config] Group.__call__() has same behaviour as Group.all() (#2018)
* Make calling groups useful

This makes config.Group.__call__ effectively an alias for Group.all(),
with the added bonus of becoming a context manager.

get_raw has been updated as well to reflect the new behaviour of
__call__.

* Fix unintended side-effects of new behaviour

* Add tests

* Add test for get_raw mixing in defaults

* Another cleanup for relying on old behaviour internally

* Fix bank relying on old behaviour

* Reformat
2018-08-26 23:30:36 +10:00
Toby Harradine
48a7a21aca [Commands] Refactor command and group decorators (#1818)
* [V3 Commands] Refactor command and group decorators

* Add some tests

* Fix docs reference

* Tweak Group's MRO
2018-08-26 23:25:25 +10:00
Toby Harradine
f595afab18 [Streams] [p]streamalert twitch channel is not for Discord channels (#2048)
Resolves #2003.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-08-26 23:18:22 +10:00
Brramble
0aca00b245 [Audio] Embed colours respect user settings (#2046)
* Embed colours now respect what the user set

* Formatting

* Get embed colour when ctx is unavailable
2018-08-26 13:09:03 +10:00
Toby Harradine
9af58d3abf [Permissions] Remove hook from is_owner check (#2053)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-08-25 21:56:40 +10:00
aikaterna
dd5ef3696f [Audio] Add thumbnail display with toggle (#1998)
* [V3 Audio] Add thumbnail display with toggle

* [V3 Audio] Add thumbnail to notify messages

* Formatting

* Update thumbnail fetching

* Update thumbnail fetching

* Track thumbnail moved to Red-Lavalink

* Formatting
2018-08-25 10:47:20 +10:00
Toby Harradine
03d49bac53 Update Red-Lavalink to version 0.1.2 (#2034)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-08-24 16:28:01 +02:00
Toby Harradine
6c082a10b1 Fix sync check causing errors (#2045)
* Fix sync check causing errors

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>

* Import timedelta...

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-08-24 16:23:52 +02:00
Michael H
77944e195a Output sanitisation (#1942)
* Add output sanitization defaults to context.send
Add some common regex filters in redbot.core.utils.common_filters
Add a wrapper for ease of use in bot.send_filtered
Sanitize ModLog Case's user field (other's considered trusted as moderator input)
Sanitize Usernames/Nicks in userinfo command.
Santize Usernames in closing of tunnels.

* Add documentation
2018-08-24 23:50:38 +10:00
Michael H
6ebfdef025 Remove self-bot support (#2008) 2018-08-24 22:51:03 +10:00
El Laggron
bc39a6741c [Launcher] append --user if the bot isn't in a venv (#2027)
* [V3 Launcher] --user CLI flag

* Better handling of the sys arguments

* Black reformat to -l 99

* Update launcher to PR#2025

* Always append --user if not in a virtualenv

* Remove --user flag
2018-08-24 22:33:57 +10:00
Michael H
bda7e08208 Handle time innacuracy by warning owner (#2036)
* handle time innacuracy by warning owner

* Fix typo and add space
2018-08-23 11:28:05 +10:00
Michael H
aa69dd381f [Core] Use autohelp properly in local blacklist/whitelist (#2042) 2018-08-23 11:22:03 +10:00
Michael H
1fd8a8e0a6 [Cleanup] Refactor internals (#2013)
* Revert "[Cleanup] Hotfix for [p]cleanup after (#2004)"

This reverts commit 7959654dc8.

* refactor cleanup

* formatting pass

* put back in try/except block
2018-08-21 12:31:48 +10:00
Caleb Johnson
1329fa1b09 [CogManager, Utils] Handle missing cogs correctly, add some helpful algorithms (#1989)
* Handle missing cogs correctly, add some helpful algorithms

For cog loading, only show "cog not found" if the module in question was the one
that failed to import. ImportErrors within cogs will show an error as they should.

- deduplicator, benchmarked to be the fastest
- bounded gather and bounded async as_completed
- tests for all additions

* Requested changes + wrap as_completed instead

So I went source diving and realized as_completed works the way I want it to,
and I don't need to reinvent the wheel for cancelling tasks that remain
if the generator is `break`ed out of. So there's that.
2018-08-21 11:26:04 +10:00
Michael H
b550f38eed [Reports] Add guild-only command check (#2016)
Resolves #2015
2018-08-17 00:00:24 +10:00
Toby Harradine
ae7b912ac8 Major dependency update (#1974)
* [V3] Stop `tmp` dir showing up

* [V3] Remove requirements.txt and declare in install_requires

* Remove requirements.txt from tox.ini

* Update and pin all dependencies and sub-dependencies

* Update for breaking changes

* Reformat

* Update docs/requirements.txt and tox.ini requirements

* Add 3.7 to identifiers and travis/tox builds

* Attempt at fixing the travis build matrix

* Attempt #2

* Attempt 3

* aiohttp.ClientSession.close() -> detach() in sync code

* Add raven-aiohttp to requirements

* Fix stuff in setup.py

 - Added discord.py back into requirements list
 - Fix typo in alabaster extra requirement

Also in the Pipfile:
 - Removed allow_prereleases and explicitly pinned black, since this is the only dep we want a prerelease for.

* Update to Rapptz/discord.py@8ccb98d395

* Add proper 3.7 build in Travis

See travis-ci/travis-ci#9815

* Which version of 3.6 does Xenial install then?

* Maybe we should stop pipenv installing useless stuff

* Nevermind, back to specific minor version

* Remove lots of WET dependency stuff

* Fix egg fragment for dependency link
2018-08-15 12:10:55 +10:00
Michael H
af9478922e Correct errormsg when using discord.ext.commands (#2021)
resolves #2020
2018-08-15 03:05:18 +10:00
Michael H
7acea29cdb [Dev] Friendlier code-block strip for [p]eval(#2017)
not relying on newlines where they aren't required by discord, retaining py highlight support.

Resolves #1928.
2018-08-14 16:31:42 +10:00
Michael H
6082eb21e3 [V3] Enforce use of redbot.core.commands (#1971)
* enforce commands as ours

* clearer user feedback

* No more 'one more tweak' commits without verifying anyway

* more detailed error with docs link + docs update

Resolves #1972.
2018-08-13 11:10:13 +10:00
Toby Harradine
a91cda4995 Version bump 3.0.0b19 (#2005) 2018-08-13 10:32:39 +10:00
Toby Harradine
7959654dc8 [Cleanup] Hotfix for [p]cleanup after (#2004)
* [Cleanup] Hotfix for [p]cleanup after

* Reformat

* Log warning for any 3rd party devs using broken helper
2018-08-12 14:46:30 +10:00
Toby Harradine
dc9a85ca98 [Streams] Add docstring for [p]streamalert list (#2001) 2018-08-12 12:09:38 +10:00
Kowlin
591ed50ac3 [Mod] Omit empty fields from [p]userinfo (#1829)
* Hide roles if none are found.

* Clarify about omitted fields in help doc
2018-08-11 13:59:43 +10:00
Toby Harradine
47350328e6 [CogManager] Correctly separate core, install and other cog paths (#1983)
* [CogManager] Correctly manage core and install paths

This keeps the core and install paths separate from those set in the
config.

It also displays the core path separately in `[p]paths`

Resolves #1982.

* [CogManager] Fix old reference to method removed in previous commit

Also did a bit of a general cleanup of cog_manager.py's code

* Make the core path a class attribute

* [CogManager] Paths should default to a list
2018-08-11 13:31:26 +10:00
Toby Harradine
75ed749cb3 [Admin] Fix [p]addrole error when a hierarchy issue occurs (#1995) 2018-08-11 12:11:15 +10:00
Toby Harradine
f44ea8b749 [ModLog] Call correct method for case message content (#1981) 2018-08-11 12:06:58 +10:00
Toby Harradine
76c0071f57 Pin lavalink to 0.1.0 (#2000) 2018-08-11 12:01:23 +10:00
El Laggron
2a396b4438 [Modlog] Make the case number optional in [p]reason (#1979)
* [V3 Modlog] Make the case number optional

If not specified, it will be the latest case.

* Fix some errors

* Black reformat

* More info for usage string

* Use isdigit instead of isnumeric
2018-08-11 11:44:17 +10:00
Toby Harradine
51a54863c5 [Streams] Don't raise KeyError on missing token (#1994)
Some streaming services don't require a token/clientID.

Resolves #1932
2018-08-10 15:07:30 +10:00
Redjumpman
06f986b92e [General] Fix lmgtfy with '+' in search (#1991) 2018-08-10 14:41:23 +10:00
Toby Harradine
652ceba845 [Bank] Raise TypeError when passing a non-int transaction amount (#1976)
* Raise TypeError when passing a non-int bank amount

* Add a test

* Add some full stops
2018-08-09 22:13:31 +10:00
Michael H
16d0f54d8f [V3 Crowdin] dont upload tox/tests/etc (#1849) 2018-08-09 15:10:30 +10:00
Caleb Johnson
872cce784a [V3 Core] Fix unload_extension for module-less cogs (#1984)
Fixes #1943

This just skips cogs when  `__module__` is None. Also:
- backported rapptz/discord.py#621
- moved RPC handler unregister to remove_cog()
- raise an exception when a load would overwrite an existing extension
2018-08-08 10:26:14 +10:00
Toby Harradine
aec3ad382a [CI] Re-enable and fix linkchecking for docs build (#1990) 2018-08-07 18:24:44 +10:00
lionirdeadman
9d4f9ef73c [General] UI improvements to [p]urban (#1871)
* Changed urban dictionary for my embed version

* Used black for formatting

* Fixed everything according to Tobotimus's review

* Fixed the description limit to 2048 characters

* Better fix adding "..." at the end if neccessary

* Add non-embed version

* Blackify
2018-08-07 16:33:39 +10:00
Caleb Johnson
cf7cafc261 [sentry] use raven-aiohttp (#1967) 2018-08-04 15:44:58 +10:00
Kowlin
e3bff7e87c Version bump v3.0.0b18 (#1959)
Administrative Merge: Solo beta release.
2018-07-25 06:15:36 +02:00
Michael H
4b19421075 [V3] use uvloop if available (#1935)
* use uvloop if available

* Update __main__.py

requested changes made
2018-07-25 05:55:55 +02:00
Michael H
cf371e8093 fix invalidation (#1945) 2018-07-25 04:44:25 +02:00
Kowlin
5eeadc6399 Commented out the link checking (#1958) 2018-07-25 04:33:43 +02:00
Redjumpman
f6823ea3d1 Update bank.py (#1937) 2018-07-25 02:57:25 +02:00
aikaterna
f24290c423 [V3 Help] Exception for help when bot is blocked (#1955)
Fix for #1901.
Administrative merge: Travis CI failed due to docs issue, see #1957
2018-07-25 02:39:51 +02:00
Michael H
f8a36885fe doc string corretion (#1944)
Administrative merge: Travis CI failed due to docs issue, see #1957
2018-07-25 02:33:17 +02:00
Kowlin
a555eff2cc Fixed a bug with the JSON Driver (#1953)
Administrative merge: Travis CI failed due to docs issue, see #1957
2018-07-25 02:18:54 +02:00
Kowlin
05c389623c Removed warnings as errors argument (#1954) 2018-07-23 02:04:30 +02:00
Michael H
bf00f5e9a2 [V3] tox match pinned version d.py (#1930) 2018-07-12 05:51:00 +02:00
aikaterna
7685c4d5d5 [V3] Bump version to 3.0.0b17 (#1929) 2018-07-12 04:21:38 +02:00
aikaterna
e701ec9617 [V3 Audio] More aggressive empty disconnect (#1925) 2018-07-12 04:14:58 +02:00
Michael H
6c1ee096a1 [V3 permissions] command usage consistency (#1905)
* make the default rule settings consistent with the rest

* update docs to match new behavior
2018-07-12 04:09:28 +02:00
aikaterna
2df282222f [V3 Audio] Fix for playlist queue duplicates (#1890)
* [V3 Audio] Fix for playlist queue duplicates

And some sanitizing of playlist names.

* [V3 Audio] Playlist naming standardization

Enforced single-word playlist name across all playlist commands, removed A-Z 0-9 name standardization. [p]playlist delete will still accept playlist names with quotes as there should be a way to remove already-existing playlists with spaces in their name.

* [V3 Audio] Black formatting
2018-07-12 04:01:11 +02:00
El Laggron
43c7bd48c7 [V3 Mod] Fix wrong reason used for modlog (#1842) 2018-07-12 03:54:32 +02:00
aikaterna
86579068d9 [V3 Admin] Clean up help strings (#1906) 2018-07-12 03:49:37 +02:00
Michael H
8e6ab9aa35 [V3 reports] Display user discrim (#1913)
* [V3 reports] Display user discrim

* Update reports.py

minor change for clarity

* format pass
2018-07-12 03:38:07 +02:00
palmtree5
77566a887a [V3 Warnings] changes to the warnings cog (#1867)
* [V3 Warnings] clarify text on entering commands

* Fix up action commands and allow for no command on both add and remove

* Notify warned user when they receive a warning + disallow warning and unwarning self

* Add myself to COOWNERS for the warnings cog
2018-07-12 03:29:22 +02:00
Redjumpman
9d0eca1914 Update economy.py (#1898) 2018-07-12 03:22:29 +02:00
Michael H
79a3164d9d [V3] Mod/admin role logic corrections (#1914)
* [V3] Mod/admin role logic corrections

* Update mod.py

* Update mod.py
2018-07-12 03:17:44 +02:00
aikaterna
eb73e48192 [V3 Audio] Respect voice channel permissions (#1878)
* [V3 Audio] Respect voice channel permissions

* [V3 Audio] Respect the user limit

* [V3 Audio] Exemption for channels with no limit
2018-07-12 03:11:51 +02:00
Eslyium
cd6af7f185 [V3 Streams] Rewording Responses/Docstrings (#1837)
* [V3 Streams] Rewording Responses/Docstrings

w/ Black if I did this right

* Readding strings
2018-07-12 03:07:15 +02:00
Michael H
3d6020b9cf [V3/permissions] Performance improvements (#1885)
* basic caching layer

* bit more work, now with an upper size to the cache

* cache fix

* smarter cache invalidation

* One more cache case

* Put in a bare skeleton of something else still needed

* more logic handling improvements

* more work, still not finished

* mass-resolve is done in theory, but needs testing

* small bugfixin + comments

* add note about before/after hooks

* LRU-dict fix

* when making comments about optimizations, provide historical context

* fmt pass
2018-07-12 02:56:08 +02:00
Michael H
461f03aac0 [V3 Context] Aliasing Colour to color (#1916)
* [V3 Context] Aliasing Colour to color

We should stay consistent here and keep the aliasing from upstream, even when we extend this.

* fmt pass
2018-07-12 02:50:59 +02:00
Michael H
35149f8837 [V3] DM usage fixes (#1919)
* DM usage fixes

* ...

* ...

* ...

* ok, formatting...
2018-07-12 02:46:14 +02:00
Toby Harradine
c0d01f32a6 [V3] Use our own checks instead of discord.py's (#1861)
* [V3] Use our own checks instead of discord.py's

* Remove bot.has_permissions checks too
2018-07-12 02:33:39 +02:00
Toby Harradine
83a0459b6a [V3] Remove all mentions of Python 3.5 (#1896)
We don't speak of him any more.
2018-07-12 02:23:18 +02:00
Toby Harradine
50f6dcef2f [V3] Stop tmp dir showing up (#1895) 2018-07-12 02:17:54 +02:00
Eslyium
5c514fd663 [V3 Reports] Rewording Responses/Docstrings (#1860)
* [v3 Reports] Rewording Responses/Docstrings

* Add the old name for [p]reportset toggle as alias

Also made some lines a bit shorter

* A few more

* Fix typo

* Clarity in [p]report docstring
2018-07-12 02:02:41 +02:00
Michael H
1c2196f78f autohelp changes. (#1836) 2018-07-12 01:23:18 +02:00
Michael H
43cc3c40f3 [V3] use configured color in all places (#1918)
* use configured color

* help formatter too
2018-07-12 00:50:46 +02:00
Michael H
7a6a4cf59d typo fix (#1917) 2018-07-08 22:18:53 +02:00
Michael H
3bcf375204 [V3] Fix duplicate help on ignore (#1862) 2018-06-25 22:00:56 +10:00
Michael H
a175bdc1c7 [V3 Cleanup] Fix cleaning up too many messages (#1864)
Resolves #1863
2018-06-25 21:45:43 +10:00
El Laggron
b557b437a3 [V3] More badges in README (#1879)
* [V3 README] More badges

* Fixed destination
2018-06-25 21:38:32 +10:00
Michael H
d1f0b59b5d [V3] Verify checks for command groups (#1882) 2018-06-25 21:33:36 +10:00
aikaterna
3ece3a1f2b [V3 Audio] Fix for not saving via playlist create (#1889) 2018-06-25 21:20:28 +10:00
aikaterna
1f1a85de18 [V3 Audio] Add checking for valid file on upload (#1891) 2018-06-25 20:32:46 +10:00
aikaterna
e08c9dafa6 [V3 Economy] Payday leaderboard clarification (#1892)
When [p]payday is used with a global bank, the number place shown on the message is relative to the global leaderboard. Since [p]leaderboard shows the server leaderboard by default, this can be confusing on a global bank as payday will most likely report a different place number than the user's server leaderboard does.
2018-06-25 20:17:44 +10:00
El Laggron
ad27607ccc [V3] --token and --no-instance flags (#1872)
* Ability to run Red with token without instance

* --no-instance flag

* Reformatted cli with black

* Fix changes requested by @Tobotimus

- Use "system reboot" to be clearer
- save_default_config renamed to create_temp_config
- More documentation for the create_temp_config function

* Update create_temp_config call

* Fix up imports
2018-06-25 19:25:23 +10:00
El Laggron
c1bcca4432 [V3 Downloader] Allow use of prefix in install message (#1869)
* [V3 Downloader] Allow to use the prefix in install msg

* Update docs

* Let's do the same for repo addition

* Fix indent

* Use replace instead of format

* Update docs
2018-06-25 12:42:47 +10:00
El Laggron
9f2ed694ce [V3 Launcher] Show version in main menu (#1810)
* [V3 Launcher] Show version in main menu

* Fix syntax

* Fix typo

* Make text underlined

* Show if red is outdated
2018-06-23 12:50:34 +10:00
Will
edadd8f2fd [V3 Config] Fix for unnecessary writes on context mgr exit (#1859)
* Fix for #1857

* And copy it so we don't have mutability issues

* Add a test
2018-06-23 12:15:22 +10:00
Will
afa08713e0 [V3] Make pytest fixtures available as a plugin (#1858)
* Move all fixtures to pytest plugin folder

* Add core dunder all

* Update other dunder all's

* Black reformat
2018-06-23 11:33:06 +10:00
Michael H
d23620727e [V3 Mod] Userinfo past nicks/names (#1865)
* [V3 Mod] Userinfo past nicks/names

Prevents trying to do a string replace on a NoneType

* address root cause as well

* remove extra whitespace that got pasted in from web editor
2018-06-20 19:49:01 +02:00
Redjumpman
b456c6ad3b [V3 Config] Fixed set_raw example in docstring (#1876)
fixed doc string
2018-06-20 11:27:36 +10:00
Tobotimus
0298b53803 [V3 JSON] Drivers deepcopy input/output data (#1855)
* [V3 JSON] Return deepcopy in JSON driver

* Add a test

* foo not bar

* Add a test for setting and then mutating

* Resolve issue for setting and mutating as well

* Reformat
2018-06-11 12:31:01 -04:00
Michael H
bfd6e4af3f [V3 Report] Remove outdated reference to tunnel.close() (#1856) 2018-06-11 22:16:41 +10:00
Michael H
31612aae4a [V3 Mod] Fix [p]modset used without a subcommand (#1854) 2018-06-11 22:02:29 +10:00
palmtree5
219367e7c1 Bump version to 3.0.0b16 (#1804) 2018-06-10 15:07:57 -08:00
Will
7b64f10fc7 [V3] Pin discord.py for beta 16 release (#1848)
* Update main requirements

* Update docs requirements

* Black reformat after version update

* Pin dpy  in docs
2018-06-10 19:01:58 -04:00
Twentysix
1ad1744054 [V3 General] Fix online user count in [p]serverinfo (#1844)
* [General] Fix online user count in [p]serverinfo

* [General] Attempt to please the black gods
2018-06-10 23:25:54 +10:00
palmtree5
7b825f2cd7 [V3] Add some tests (#1590)
* Add some tests related to economy

* Add a test for repo removal

* black style formatting
2018-06-09 22:00:21 -04:00
aikaterna
3759fce090 [V3 Audio] Empty channel disconnect setting (#1832)
* [V3 Audio] Empty channel disconnect setting

* [V3 Audio] Small fixes

* [V3 Audio] Remove unused variable

* [V3 Audio] Timer task
2018-06-09 21:55:28 -04:00
Will
470521f7c8 [V3 DataConverter] Fix past nicks conversion for mod (#1840)
* Fix existing tests permanently modifying attributes

* Add dataconverter tests file

* Fix past nicks spec converter

* Update gitignore for dataconverter data files

* Add data file for dataconverter test

* Simplify fix
2018-06-09 20:27:06 -04:00
Tobotimus
a070dffb93 [V3 Streams] Fix streams race condition (#1834) 2018-06-10 00:12:58 +10:00
Will
9e7bc94aab Catch another error on windows compiler failure (#1830) 2018-06-09 03:48:03 +02:00
Tobotimus
033d0113a5 [V3] Send meaningful responses on conversion failure (#1817)
* [V3] Send meaningful responses on conversion failures

* Replace existing `discord.ext.commands` imports

Just to be sure

* Better Permissions converter response
2018-06-08 21:20:40 -04:00
Tobotimus
d0a53ed2df [V3 Core] Fix backup error (#1820) 2018-06-08 21:12:16 -04:00
Michael H
49b80e9fe3 [V3 Report] Patch issue with attachment grabbing (#1822)
* This fixes the issue on report's side

* This prevents delete_delay from causing future issues where message objects are needed

* black format pass

* use the tools we have to clean this logic up a lot
2018-06-08 21:08:00 -04:00
Michael H
d5f5ddbec5 [V3 Help] Fix formatter field pagination (#1813)
* fix prefix

* help formatter pagination fix

* index comp fix
2018-06-08 21:02:28 -04:00
Michael H
17c7dd658d [V3 Core] Command group automatic help (#1790)
* decorator inheritence

* black format

* add autohelp

* modify commands to use autohelp
2018-06-08 20:54:36 -04:00
Will
ca19ecaefc [V3 Downloader] Add a requirements list to cog info (#1827) 2018-06-08 20:48:46 -04:00
Tobotimus
c149f00f82 [V3 Menu] Don't block on adding reactions (#1808)
* [V3 Menu] Don't block on adding reactions

* Add a comment
2018-06-08 20:43:42 -04:00
Will
b041d59fc7 [V3 Downloader] Make hidden hidden and add disabled (#1828)
* Make hidden hidden and add disabled

* Add documentation
2018-06-08 20:39:07 -04:00
Will
b983d5904b [V3 RPC] Swap back to initial RPC library and hook into core commands (#1780)
* Switch RPC libs for websockets support

* Implement RPC handling for core

* Black reformat

* Fix docs for build on travis

* Modify RPC to use a Cog base class

* Refactor rpc server reference as global

* Handle cogbase unload method

* Add an init call to handle mutable base attributes

* Move RPC server reference back to the bot object

* Remove unused import

* Add tests for rpc method add/removal

* Add tests for rpc method add/removal and cog base unloading

* Add one more test

* Black reformat

* Add RPC mixin...fix MRO

* Correct internal rpc method names

* Add rpc test html file for debugging/example purposes

* Add documentation

* Add get_method_info

* Update docs with an example RPC call specifying parameter formatting

* Make rpc methods UPPER

* Black reformat

* Fix doc example

* Modify this to match new method naming convention

* Add more tests
2018-06-08 20:31:38 -04:00
Michael H
8b15053dd4 [V3 Mod/Modlog] prevent self-casing the bot + feedback for heirarchy (#1777)
* prevent the bot from being a modlog target

* prevent heirarchy issues in mod

* modify this comparison to avoid more complex mocking of the guild object in mod test

* spelling
2018-06-08 20:27:07 -04:00
Tobotimus
e15815cd97 [V3 Downloader] Don't do 3rd party agreement without command args (#1821) 2018-06-08 20:18:51 -04:00
palmtree5
94a64d8fae [V3 Downloader] Split available and installed cogs (#1826) 2018-06-08 19:58:20 -04:00
Michael H
fd7088de1a [V3 Help formatter] Better name-as-prefix handling (#1823)
* prefix handling

* actually, integration role isn't a valid way
2018-06-08 11:11:44 -04:00
Will
7d4946560d [V3] Fix typo in load (#1814) 2018-06-08 10:56:03 -04:00
Michael H
b7c9647e1a [V3] Fix dm help set (#1806) 2018-06-07 01:23:26 -04:00
Will
36b9f64aae [V3 Alias] Fix missing await (#1805) 2018-06-07 14:53:12 +10:00
Eslyium
60a72b2ba4 [V3] Cleanup quotes in cogs (#1782)
* Cleanup quotes in cogs

* More quote cleanup that I missed

fixed a little bit of grammar here and there as well.

* [V3 Warnings] Change allowcustomreasons docstring

To help not confuse users who would believe that the command would use allow or disallow.

* Run black reformat
2018-06-07 00:42:59 -04:00
jjay12365
f830f73ae6 [V3 Streams] Fixed issue with making YT Stream Embed (#1812)
fixed error not allowing bot to make yt stream embed
2018-06-06 14:57:25 -04:00
Michael H
95f51e1126 [V3] Add missing await for [p]set prefix (#1809) 2018-06-06 17:08:33 +10:00
Michael H
8916f55d52 [V3] permissions canrun fix (#1787)
* permissions canrun fix

* Missing await

* async gen
2018-06-05 12:33:45 -08:00
Michael H
4aaef9558a [V3 Core] local whitelist/blacklist (#1776)
* implements local whitelist/blacklist which had unused bot.db settings

This includes a role listing

* format pass

* Update core_commands.py

* .

* black format pass
2018-06-05 12:19:44 -08:00
palmtree5
0b78664792 [V3 Fuzzy search] fix several issues with this feature (#1788)
* [V3 Fuzzy search] fix several issues with this feature

* Make it check if parent commands are hidden

* Check if compiler available in setup.py

* Let's just compile a dummy C file to check compiler availability

* Add a missing import + remove unneeded code
2018-06-05 22:14:11 +02:00
Michael H
db5d4d5158 [V3 launcher] token can be an empty string (#1794)
* token can be an empty string

* I like this sytlistically more
2018-06-05 11:59:42 -08:00
Tobotimus
0dfd8b6453 [V3 Docs] Add intersphinx link to discord.ext.commands api reference (#1764) 2018-06-03 17:32:25 -08:00
rngesus-wept
11a2fb1088 [V3 Core] Fix display of whitelist and blacklist members (#1789) 2018-06-03 21:41:36 +10:00
Michael H
40feeff442 [V3 core.commands] decorator inheritence fix (#1786)
* decorator inheritence

* black format
2018-06-02 18:34:30 -08:00
Michael H
a0a2976e0a [V3] Fixes issue preventing token reset from setup (#1771)
* fixes issue preventing token reset.

Also removes a faulty assumption about not needing cleanup tasks

* Update __main__.py

remove unneeded condition
2018-06-02 18:43:26 -04:00
Michael H
741f3cbdcc [V3 CLI] CLI prefix args correctly display in the on_ready print (#1770) 2018-06-02 18:36:13 -04:00
Michael H
a6965c4b5a [V3] Hide help command from help (#1772) 2018-06-02 18:29:16 -04:00
Tobotimus
19b05e632c [V3] Use typing.TYPE_CHECKING instead of utils.TYPE_CHECKING (#1778)
Not needed any more as we no longer support python<3.6
2018-06-02 18:24:16 -04:00
Michael H
8610b47a68 [V3 Admin] Announce ignore parameter modification (#1781) 2018-06-02 18:20:01 -04:00
aikaterna
2ab8890540 [V3 Audio] Check for empty queue in [p]skip (#1769)
Privileged users outside of the channel could invoke skip with an empty queue.
2018-06-02 18:15:10 -04:00
Michael H
5de5a519c3 [V3 Permissions] Don't rely on load order to be consistent (#1760)
* Modifies permissions re #1758

* requested changes in
2018-06-02 18:10:40 -04:00
Tobotimus
0d193d3e9e [V3] Make bot send typing whilst loading cogs (#1756)
* Show bot is responsive during cog load

* Log download of Lavalink.jar event

* Fix #1709's other bug

* Reformat

* Update core_commands.py from merge
2018-06-02 18:06:10 -04:00
Tobotimus
622382f425 [V3] Clean up some ugly auto-formatted strings (#1753)
* [V3] Cleanup some ugly auto-formatted strings

* Reformat
2018-06-02 18:01:14 -04:00
Tobotimus
c1f09326cc [V3] Use sys.exit() over exit() (#1755) 2018-06-02 17:56:28 -04:00
Tobotimus
ddbbba4aaa [V3] Ignore .idea/ directory entirely (#1754)
* [V3] Ignore .idea folder entirely

Since we're not including anything from it currently, so there's no reason not to ignore it.

* Ignore IDEA project files
2018-06-02 11:55:34 +10:00
Tobotimus
bcf7ea30c5 [V3 Core] Add a much-needed forward reference (#1763) 2018-06-02 11:20:06 +10:00
Will
35e9fab701 [V3 Admin] Add notes about case sensitivity for selfrole (#1762) 2018-06-02 11:01:08 +10:00
Will
864b6d313e [V3 Core Commands] Refactor some commands for testing/RPC (#1691)
* Extract load/unload/reload

* Add a few more commands

* Refactor load/unload signature

* Add invite URL and version info

* Black fixes

* Split the incoming cog names in reload correctly

* Reformat

* Remove meta.bot
2018-06-02 10:49:59 +10:00
aikaterna
d47d12e961 [V3 Audio] Restrict check for reactions on [p]now (#1752) 2018-06-02 02:44:30 +02:00
Michael H
9f0e752318 [V3 Core] Fix error on [p]set command when used in DM (#1748)
* issue #1741 fix

* requested changes
2018-06-02 09:26:12 +10:00
palmtree5
34bd5ead15 [V3] Drop 3.5 support (#1721) 2018-06-01 19:20:21 -04:00
Redjumpman
1fd5dffdc7 [V3/Misc] Spelling, Grammar, and doc string fixes. (#1747)
* Update streams.py

* Update filter.py

* Update permissions.py

* Update customcom.py

* Update image.py

* Update trivia.py

* Update warnings.py
2018-06-01 19:20:12 +10:00
Michael H
6d7a900bbb [V3] Use Embed.Empty for unset embed colour (#1750) 2018-05-31 14:31:44 +10:00
Tobotimus
fb4f921159 [V3 Docs] Pin RTD yarl version (#1744) 2018-05-29 18:42:30 -08:00
Tobotimus
14cc701b25 [V3] Update black version and reformat (#1745)
* Update black version and reformat

* Pin black in extras_require
2018-05-29 18:37:00 -08:00
Tobotimus
d8c4113d24 Remove 3.5 from tox environments (#1740) 2018-05-30 02:18:12 +02:00
Michael H
9eb6bb7738 [V3 permissions] more docs + minor addition. (#1737)
* more info

* docstring
2018-05-28 12:02:42 -08:00
Michael H
de96f8b9f9 Fixed Readme (#1730)
* twine

* twine

* twine

* twine
2018-05-28 19:03:50 +02:00
212 changed files with 28174 additions and 4584 deletions

3
.github/CODEOWNERS vendored
View File

@@ -24,6 +24,8 @@ redbot/core/utils/mod.py @palmtree5
redbot/core/utils/data_converter.py @mikeshardmind redbot/core/utils/data_converter.py @mikeshardmind
redbot/core/utils/antispam.py @mikeshardmind redbot/core/utils/antispam.py @mikeshardmind
redbot/core/utils/tunnel.py @mikeshardmind redbot/core/utils/tunnel.py @mikeshardmind
redbot/core/utils/caching.py @mikeshardmind
redbot/core/utils/common_filters.py @mikeshardmind
# Cogs # Cogs
redbot/cogs/admin/* @tekulvw redbot/cogs/admin/* @tekulvw
@@ -44,6 +46,7 @@ redbot/cogs/trivia/* @Tobotimus
redbot/cogs/dataconverter/* @mikeshardmind redbot/cogs/dataconverter/* @mikeshardmind
redbot/cogs/reports/* @mikeshardmind redbot/cogs/reports/* @mikeshardmind
redbot/cogs/permissions/* @mikeshardmind redbot/cogs/permissions/* @mikeshardmind
redbot/cogs/warnings/* @palmtree5
# Docs # Docs
docs/* @tekulvw @palmtree5 docs/* @tekulvw @palmtree5

View File

@@ -31,7 +31,7 @@ We love receiving contributions from our community. Any assistance you can provi
# 2. Ground Rules # 2. Ground Rules
We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub) as our main source of collaboration and coordination. Your experience contributing to Red will be greatly improved if you go get that plugin. We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub) as our main source of collaboration and coordination. Your experience contributing to Red will be greatly improved if you go get that plugin.
1. Ensure cross compatibility for Windows, Mac OS and Linux. 1. Ensure cross compatibility for Windows, Mac OS and Linux.
2. Ensure all Python features used in contributions exist and work in Python 3.5 and above. 2. Ensure all Python features used in contributions exist and work in Python 3.6 and above.
3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning: 3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning:
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally. 4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea. 5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
@@ -79,7 +79,7 @@ Note: If you haven't used `pipenv` before but are comfortable with virtualenvs,
We've recently started using [tox](https://github.com/tox-dev/tox) to run all of our tests. It's extremely simple to use, and if you followed the previous section correctly, it is already installed to your virtual environment. We've recently started using [tox](https://github.com/tox-dev/tox) to run all of our tests. It's extremely simple to use, and if you followed the previous section correctly, it is already installed to your virtual environment.
Currently, tox does the following, creating its own virtual environments for each stage: Currently, tox does the following, creating its own virtual environments for each stage:
- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on both python 3.5 and 3.6 (test environments `py35` and `py36` respectively) - Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on python 3.6 (test environment `py36`)
- Ensures documentation builds without warnings, and all hyperlinks have a valid destination (test environment `docs`) - Ensures documentation builds without warnings, and all hyperlinks have a valid destination (test environment `docs`)
- Ensures that the code meets our style guide with [black](https://github.com/ambv/black) (test environment `style`) - Ensures that the code meets our style guide with [black](https://github.com/ambv/black) (test environment `style`)
@@ -94,8 +94,6 @@ Our style checker of choice, [black](https://github.com/ambv/black), actually ha
Use the command `black --help` to see how to use this tool. The full style guide is explained in detail on [black's GitHub repository](https://github.com/ambv/black). **There is one exception to this**, however, which is that we set the line length to 99, instead of black's default 88. When using `black` on the command line, simply use it like so: `black -l 99 <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 <src>`.
Note: Python 3.6+ is required to install and run black. If you installed your development environment with Python 3.5, black will not be installed.
### 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:
1. `make reformat`: Reformat all python files in the project with Black 1. `make reformat`: Reformat all python files in the project with Black

30
.gitignore vendored
View File

@@ -1,40 +1,16 @@
# Trivia list repo injection
redbot/trivia/
*.json *.json
*.exe *.exe
*.dll *.dll
*.pot
.data .data
!/tests/cogs/dataconverter/data/**/*.json
### JetBrains template ### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff: # User-specific stuff:
.idea/**/workspace.xml .idea/
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws *.iws
## Plugin-specific files: ## Plugin-specific files:

View File

@@ -4,10 +4,11 @@ formats:
build: build:
image: latest image: latest
requirements_file: docs/requirements.txt requirements_file: dependency_links.txt
python: python:
version: 3.6 version: 3.6
pip_install: true pip_install: true
extra_requirements: extra_requirements:
- docs
- mongo - mongo

View File

@@ -1,33 +1,37 @@
dist: trusty dist: xenial
language: python language: python
cache: pip cache: pip
notifications: notifications:
email: false email: false
sudo: true
python: python:
- 3.6.5 - 3.6.6
- 3.7
env: env:
global: global:
PIPENV_IGNORE_VIRTUALENVS=1 PIPENV_IGNORE_VIRTUALENVS=1
matrix: matrix:
- TOXENV=py TOXENV=py
- TOXENV=docs
- TOXENV=style
install: install:
- pip install --upgrade pip pipenv - pip install --upgrade pip tox
- pipenv install --dev
script: script:
- pipenv run tox - tox
jobs: jobs:
include: include:
- python: 3.6.6
env: TOXENV=docs
- python: 3.6.6
env: TOXENV=style
# These jobs only occur on tag creation for V3/develop if the prior ones succeed # These jobs only occur on tag creation for V3/develop if the prior ones succeed
- stage: PyPi Deployment - stage: PyPi Deployment
if: tag IS present if: tag IS present
python: 3.6.5 python: 3.6.6
env: env:
- DEPLOYING=true - DEPLOYING=true
deploy: deploy:
@@ -39,11 +43,11 @@ jobs:
on: on:
repo: Cog-Creators/Red-DiscordBot repo: Cog-Creators/Red-DiscordBot
branch: V3/develop branch: V3/develop
python: 3.6.5 python: 3.6.6
tags: true tags: true
- stage: Crowdin Deployment - stage: Crowdin Deployment
if: tag IS present if: tag IS present
python: 3.6.5 python: 3.6.6
env: env:
- DEPLOYING=true - DEPLOYING=true
before_deploy: before_deploy:
@@ -51,12 +55,13 @@ jobs:
- echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list - echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list
- sudo apt-get update -qq - sudo apt-get update -qq
- sudo apt-get install -y crowdin - sudo apt-get install -y crowdin
- pip install redgettext==2.1
deploy: deploy:
- provider: script - provider: script
script: python3 ./generate_strings.py script: make gettext
skip_cleanup: true skip_cleanup: true
on: on:
repo: Cog-Creators/Red-DiscordBot repo: Cog-Creators/Red-DiscordBot
branch: V3/develop branch: V3/develop
python: 3.6.5 python: 3.6.6
tags: true tags: true

View File

@@ -1,5 +1,3 @@
include README.rst include README.rst
include LICENSE include LICENSE
include requirements.txt include dependency_links.txt
include discord/bin/*.dll
include redbot/cogs/audio/application.yml

View File

@@ -2,3 +2,6 @@ reformat:
black -l 99 `git ls-files "*.py"` black -l 99 `git ls-files "*.py"`
stylecheck: stylecheck:
black --check -l 99 `git ls-files "*.py"` black --check -l 99 `git ls-files "*.py"`
gettext:
redgettext --command-docstrings --verbose --recursive redbot --exclude-files "redbot/pytest/**/*"
crowdin upload

14
Pipfile
View File

@@ -4,17 +4,9 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
"discord.py" = { git = 'git://github.com/Rapptz/discord.py', ref = 'rewrite', editable = true} "discord.py" = { git = 'git://github.com/Rapptz/discord.py', ref = 'rewrite', editable = true }
"e1839a8" = {path = ".", editable = true} "e1839a8" = { path = ".", editable = true, extras = ['mongo', 'voice'] }
[dev-packages] [dev-packages]
tox = "*" tox = "*"
pytest = "*" "e1839a9" = { path = ".", editable = true, extras = ['docs', 'test', 'style'] }
pytest-asyncio = "*"
sphinx = ">1.7"
sphinxcontrib-asyncio = "*"
sphinx-rtd-theme = "*"
black = {version = "*", python_version = ">= '3.6'"}
[pipenv]
allow_prereleases = true

529
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "d340e4a19777736703970e45766d05d67b973db38b87382b6ef8696cb53abb60" "sha256": "edd35f353e1fadc20094e40de6627db77fd61303da01794214c44d748e99838b"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@@ -16,21 +16,30 @@
"default": { "default": {
"aiohttp": { "aiohttp": {
"hashes": [ "hashes": [
"sha256:129d83dd067760cec3cfd4456b5c6d7ac29f2c639d856884568fd539bed5a51f", "sha256:1a112a1fdf3802b7f2b182e22e51d71e4a8fa7387d0d38e79a268921b869e384",
"sha256:33c62afd115c456b0cf1e890fe6753055effe0f31a28321efd4f787378d6f4ab", "sha256:33aa7c937ebaf063a860cbb0c263a771b33333a84965c6148eeafe64fb4e29ca",
"sha256:666756e1d4cf161ed1486b82f65fdd386ac07dd20fb10f025abf4be54be12746", "sha256:550b4a0788500f6d00f41b7fdd9fcce6d78f99706a7b2f6f81d4d331c7ca468e",
"sha256:9705ded5a0faa25c8f14c6afb7044002d66c9120ed7eadb4aa9ca4aad32bd00c", "sha256:601e8e83123b4d423a9dfddf7d6943f4f520651a78ffcd50c99d065136c7ff7b",
"sha256:af5bfdd164256118a0a306b3f7046e63207d1f8cba73a67dcc0bd858dcfcd3bc", "sha256:620f19ba7628b70b177f5c2e6a55a6fd6e7c8591cde38c3f8f52551733d31b66",
"sha256:b80f44b99fa3c9b4530fcfa324a99b84843043c35b084e0b653566049974435d", "sha256:70d56c784da1239c89d39fefa166fd429306dada641178389be4184a9c04e501",
"sha256:c67e105ec74b85c8cb666b6877569dee6f55b9548f982983b9bee80b3d47e6f3", "sha256:7de2c9e445a5d257935011268202338538abef1aaff341a4733eca56419ca6f6",
"sha256:d15c6658de5b7783c2538407278fa062b079a46d5f814a133ae0f09bbb2cfbc4", "sha256:96bb80b659cc2bafa160f3f0c346ce7fc10de1ffec4908d7f9690797f155f658",
"sha256:d611ebd1ef48498210b65486306e065fde031040a1f3c455ca1b6baa7bf32ad3", "sha256:ae7501cc6a6c37b8d4774bf2218c37be47fe42019a2570e8510fc2044e59d573",
"sha256:dcc7e4dcec6b0012537b9f8a0726f8b111188894ab0f924b680d40b13d3298a0", "sha256:c833aa6f4c9ac3e3eb843e3d999bae51339ad33a937303f43ce78064e61cb4b6",
"sha256:de8ef106e130b94ca143fdfc6f27cda1d8ba439462542377738af4d99d9f5dd2", "sha256:dd81d85a342edf3d2a388e2f24d9facebc9c04550043888f970ee2f228c93059",
"sha256:eb6f1405b607fff7e44168e3ceb5d3c8a8c5a2d3effe0a27f843b16ec047a6d7", "sha256:f20deec7a3fbaec7b5eb7ad99878427ad2ee4cc16a46732b705e8121cbb3cc12",
"sha256:f0e2ac69cb709367400008cebccd5d48161dd146096a009a632a132babe5714c" "sha256:f52e7287eb9286a1e91e4c67c207c2573147fbaddc68f70efb5aeee5d1992f2e",
"sha256:fe7b2972ff7e779e812f974aa5695edc328ecf559ceeea887ac46f06f090ad4c",
"sha256:ff1447c84a02b9cd5dd3a9332d1fb181a4386c3625765bb5caf1cfbc210ab3f9"
], ],
"version": "==2.2.5" "version": "==3.3.2"
},
"aiohttp-json-rpc": {
"hashes": [
"sha256:bf1eb7e30949b60f74cb84731b5676bd7dc3f0298056ddbbe989d9219260008c",
"sha256:e1ae47d522a7857c612be8ba447cec3cad8c8b7d628353289a0889a1135166c8"
],
"version": "==0.11"
}, },
"appdirs": { "appdirs": {
"hashes": [ "hashes": [
@@ -41,10 +50,18 @@
}, },
"async-timeout": { "async-timeout": {
"hashes": [ "hashes": [
"sha256:00cff4d2dce744607335cba84e9929c3165632da2d27970dbc55802a0c7873d0", "sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c",
"sha256:9093db5b8ddbe4b8f6885d1a6e0ad84ae3155464cbf6877c387605244c285f3c" "sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287"
], ],
"version": "==2.0.1" "markers": "python_version >= '3.5.3'",
"version": "==3.0.0"
},
"attrs": {
"hashes": [
"sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265",
"sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b"
],
"version": "==18.1.0"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
@@ -63,7 +80,7 @@
"discord.py": { "discord.py": {
"editable": true, "editable": true,
"git": "git://github.com/Rapptz/discord.py", "git": "git://github.com/Rapptz/discord.py",
"ref": "rewrite" "ref": "8ccb98d395537b1c9acc187e1647dfdd07bb831b"
}, },
"distro": { "distro": {
"hashes": [ "hashes": [
@@ -74,14 +91,11 @@
}, },
"e1839a8": { "e1839a8": {
"editable": true, "editable": true,
"path": "." "extras": [
}, "mongo",
"funcsigs": { "voice"
"hashes": [
"sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca",
"sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"
], ],
"version": "==1.0.2" "path": "."
}, },
"fuzzywuzzy": { "fuzzywuzzy": {
"hashes": [ "hashes": [
@@ -92,23 +106,23 @@
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4" "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
], ],
"version": "==2.6" "version": "==2.7"
}, },
"jsonrpcserver": { "idna-ssl": {
"hashes": [ "hashes": [
"sha256:ab8013cdee3f65d59c5d3f84c75be76a3492caa0b33ecaa3f0f69906cf3d9e92" "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
], ],
"version": "==3.5.4" "version": "==1.1.0"
}, },
"jsonschema": { "motor": {
"hashes": [ "hashes": [
"sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08", "sha256:462fbb824f4289481c158227a2579d6adaf1ec7c70cf7ebe60ed6ceb321e5869",
"sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02" "sha256:d035c09ab422bc50bf3efb134f7405694cae76268545bd21e14fb22e2638f84e"
], ],
"version": "==2.6.0" "version": "==2.0.0"
}, },
"multidict": { "multidict": {
"hashes": [ "hashes": [
@@ -126,8 +140,44 @@
"sha256:d870f399fcd58a1889e93008762a3b9a27cf7ea512818fc6e689f59495648355", "sha256:d870f399fcd58a1889e93008762a3b9a27cf7ea512818fc6e689f59495648355",
"sha256:e9404e2e19e901121c3c5c6cffd5a8ae0d1d67919c970e3b3262231175713068" "sha256:e9404e2e19e901121c3c5c6cffd5a8ae0d1d67919c970e3b3262231175713068"
], ],
"markers": "python_version >= '3.4.1'",
"version": "==4.3.1" "version": "==4.3.1"
}, },
"pymongo": {
"hashes": [
"sha256:08dea6dbff33363419af7af3bf2e9a373ff71eb22833dd7063f9b953f09a0bdf",
"sha256:0949110db76eb1b54cecfc0c0f8468a8b9a7fd42ba23fd0d4a37d97e0b4ca203",
"sha256:0c31a39f440801cc8603547ccaacf4cb1f02b81af6ba656621c13677b27f4426",
"sha256:1e10b3fda5677d360440ebd12a1185944dc81d9ea9acf0c6b0681013b3fb9bc2",
"sha256:1f59440b993666a417ba1954cfb1b7fb11cb4dea1a1d2777897009f688d000ee",
"sha256:2b5a3806d9f656c14e9d9b693a344fc5684fdd045155594be0c505c6e9410a94",
"sha256:4a14e2d7c2c0e07b5affcfbfc5c395d767f94bb1a822934a41a3b5371cde1458",
"sha256:4cb50541225208b37786fdb0de632e475c4f00ec4792579df551ef48d6999d69",
"sha256:52999666ad01de885653e1f74a86c2a6520d1004afec475180bebf3d7393a8fc",
"sha256:562c353079e8ce7e2ad611fd7436a72f5df97be72bca59ae9ebf789a724afd5c",
"sha256:5ce2a71f473f4703daa8d6c61a00b35ce625a7f5015b4371e3af728dafca296a",
"sha256:6613e633676168a4500e5e6bb6e3e64d3fdb96d2dc472eb4b99235fb4141adb1",
"sha256:8330406f294df118399c721f80979f2516447bcc73e4262826687872c864751e",
"sha256:8e939dfa7d16609b99eb4d1fd2fc74f7a90f4fd0aaf31d611822daaff456236f",
"sha256:8fa4303e1f50d9f0c8f2f7833b5a370a94d19d41449def62b34ae072126b4dfd",
"sha256:966d987975aa3b4cfcdf1495930ff6ecb152fafe8e544e40633e41b24ca3e1c5",
"sha256:aec4ea43a1b8e9782246a259410f66692f2d3aa0f03c54477e506193b0781cb6",
"sha256:b73f889f032fbef05863f5056b46468a8262ae83628898e20b10bbbb79a3617e",
"sha256:b752088a2f819f163d11dfdbbe627b27eef9d8478c7e57d42c5e7c600fee434e",
"sha256:c8669f96277f140797e0ff99f80bd706271674942672a38ed694e2bfa66f3900",
"sha256:ccf00549efaf6f8d5b35b654beb9aed2b788a5b33b05606eb818ddaa4e924ea3",
"sha256:ce7c91463ad21ac72fc795188292b01c8366cf625e2d1e5ed473ce127b844f60",
"sha256:d776d8d47884e6ad39ff8a301f1ae6b7d2186f209218cf024f43334dbba79c64",
"sha256:dab0f63841aebb2b421fadb31f3c7eef27898f21274a8e5b45c4f2bccb40f9ed",
"sha256:daedcfbf3b24b2b687e35b33252a9315425c2dd06a085a36906d516135bdd60e",
"sha256:e7ad1ec621db2c5ad47924f63561f75abfd4fff669c62c8cc99c169c90432f59",
"sha256:f14fb6c4058772a0d74d82874d3b89d7264d89b4ed7fa0413ea0ef8112b268b9",
"sha256:f16c7b6b98bc400d180f05e65e2236ef4ee9d71f3815280558582670e1e67536",
"sha256:f2d9eb92b26600ae6e8092f66da4bcede1b61a647c9080d6b44c148aff3a8ea4",
"sha256:ffe94f9d17800610dda5282d7f6facfc216d79a93dd728a03d2f21cff3af7cc6"
],
"version": "==3.7.1"
},
"python-levenshtein": { "python-levenshtein": {
"hashes": [ "hashes": [
"sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" "sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1"
@@ -136,94 +186,117 @@
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8", "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
"sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736", "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
"sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f", "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
"sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608", "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
"sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8", "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
"sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab", "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
"sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7", "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
"sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3", "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
"sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1", "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
"sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6", "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
"sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8", "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
"sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4",
"sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca",
"sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269"
], ],
"version": "==3.12" "version": "==3.13"
}, },
"raven": { "raven": {
"hashes": [ "hashes": [
"sha256:0adae40e004dfe2181d1f2883aa3d4ca1cf16dbe449ae4b445b011c6eb220a90", "sha256:3fd787d19ebb49919268f06f19310e8112d619ef364f7989246fc8753d469888",
"sha256:84da75114739191bdf2388f296ffd6177e83567a7fbaf2701e034ad6026e4f3b" "sha256:95f44f3ea2c1b176d5450df4becdb96c15bf2632888f9ab193e9dd22300ce46a"
], ],
"version": "==6.5.0" "version": "==6.9.0"
}, },
"red-trivia": { "raven-aiohttp": {
"hashes": [ "hashes": [
"sha256:39413b9fb3f9b9362d6de1dcf69a4bf635b0f3518243f7178299b96d26cbb6a7" "sha256:1444a49c93a85b8bb57c6ee649e512368dce7a26ad64ac3a01d86aa5669d77f3",
"sha256:6a34b6a9841ad0fd827eeb158edb5826c5c5bd7babe2cde2a3f23eb85313af04"
], ],
"version": "==1.1.1" "version": "==0.7.0"
}, },
"six": { "red-lavalink": {
"hashes": [ "hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", "sha256:6a1a34471ccf4630eee537049568dd87e8e93614f1d1ce355dd74e5b10079782"
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
], ],
"version": "==1.11.0" "version": "==0.1.2"
}, },
"websockets": { "websockets": {
"hashes": [ "hashes": [
"sha256:09dfec40e9b73e8808c39ecdbc1733e33915a2b26b90c54566afc0af546a9ec3", "sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
"sha256:2aa6d52264cecb08d39741e8fda49f5ac4872aef02617230c84d02e861f3cc5a", "sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6",
"sha256:2f5b7f3920f29609086fb0b63552bb1f86a04b8cbdcc0dbf3775cc90d489dfc8", "sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1",
"sha256:3d38f76f71654268e5533b45df125ff208fee242a102d4b5ca958da5cf5fb345", "sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538",
"sha256:3fcc7dfb365e81ff8206f950c86d1e73accdf3be2f9110c0cb73be32d2e7a9a5", "sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4",
"sha256:4128212ab6f91afda03a0c697add261bdf6946b47928db83f07298ea2cd8d937", "sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908",
"sha256:43e5b9f51dd0000a4c6f646e2ade0c886bd14a784ffac08b9e079bd17a63bcc5", "sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0",
"sha256:4a932c17cb11c361c286c04842dc2385cc7157019bbba8b64808acbc89a95584", "sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d",
"sha256:5ddc5fc121eb76771e990f071071d9530e27d20e8cfb804d9f5823de055837af", "sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c",
"sha256:7347af28fcc70eb45be409760c2a428f8199e7f73c04a621916c3c219ed7ad27", "sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d",
"sha256:85ae1e4b36aa2e90de56d211d2de36d7c093d00277a9afdd9b4f81e69c0214ab", "sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c",
"sha256:8a29100079f5b91a72bcd25d35a7354db985d3babae42d00b9d629f9a0aaa8ac", "sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb",
"sha256:a7e7585c8e3c0f9277ad7d6ee6ccddc69649cd216255d5e255d68f90482aeefa", "sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf",
"sha256:aa42ecef3aed807e23218c264b1e82004cdd131a6698a10b57fc3d8af8f651fc", "sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e",
"sha256:b19e7ede1ba80ee9de6f5b8ccd31beee25402e68bef7c13eeb0b8bc46bc4b7b7", "sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96",
"sha256:c4c5b5ce2d66cb0cf193c14bc9726adca095febef0f7b2c04e5e3fa3487a97a4", "sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584",
"sha256:de743ef26b002efceea7d7756e99e5d38bf5d4f27563b8d27df2a9a5cc57340a", "sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484",
"sha256:e1e568136ad5cb6768504be36d470a136b072acbf3ea882303aee6361be01941", "sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d",
"sha256:e8992f1db371f2a1c5af59e032d9dc7c1aa92f16241efcda695b7d955b4de0c2", "sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559",
"sha256:e9c1cdbb591432c59d0b5ca64fd30b6d517024767f152fc169563b26e7bcc9da" "sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
], ],
"version": "==3.4" "markers": "python_version >= '3.4'",
"version": "==6.0"
}, },
"yarl": { "yarl": {
"hashes": [ "hashes": [
"sha256:605480ee43eead69ec8e8c52cdfefc79cef6379cc0e87d908cf290408c1e49af", "sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9",
"sha256:7fad2530cb4ddf2b74c1e4f6f9f0e28eac482094c6542f98fd71ecf67fb4fded", "sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee",
"sha256:837d866a70f1ea03005914a740bddea89a253afabd6589db981b91738768bd25", "sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308",
"sha256:885e40812ff9fc80e6f28ef04ad6396e3ae583ab504b1a76301fdcec7fc9f30f", "sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357",
"sha256:a5457e075eab1170141774a8c69906c223ea0088eaebd6ef91b04b33527fa905", "sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78",
"sha256:baa0d3f7982fa0c03a55433109c405e79a597141f2e2d6ee7e16c03eabd74886", "sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8",
"sha256:beeefbe0edd47fc8b657bf7bf44791f7a6e5b14f3de1846daf999687cb68c156", "sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1",
"sha256:cf6a3d6fd3e79d3457d520c12d5d18b030d5ca5d0b205ca6481857804d8d944d", "sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4",
"sha256:d07d3dc6849345b7437dc58ea49ad2a1960017386d86288550728ca38e482ddc", "sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7"
"sha256:d81e45bedefccb97e4e8f7d32cfae0af1d9eadd1ae795fc420c8319c3dab2a28",
"sha256:e1da2853a92fbc7e2d0248bbfa931cd621121e70ce6dda7c1eeef3516d51b46c",
"sha256:f1201de3e93fb1efc3111c8928d9366875edefd65d77c0f6b847fe299e8e1122",
"sha256:fe0390a29b5c7e90975feefe863e3d3a851be546bd797b23f338d24a15efa920"
], ],
"version": "==0.18.0" "markers": "python_version >= '3.4.1'",
"version": "==1.2.6"
} }
}, },
"develop": { "develop": {
"aiohttp": {
"hashes": [
"sha256:1a112a1fdf3802b7f2b182e22e51d71e4a8fa7387d0d38e79a268921b869e384",
"sha256:33aa7c937ebaf063a860cbb0c263a771b33333a84965c6148eeafe64fb4e29ca",
"sha256:550b4a0788500f6d00f41b7fdd9fcce6d78f99706a7b2f6f81d4d331c7ca468e",
"sha256:601e8e83123b4d423a9dfddf7d6943f4f520651a78ffcd50c99d065136c7ff7b",
"sha256:620f19ba7628b70b177f5c2e6a55a6fd6e7c8591cde38c3f8f52551733d31b66",
"sha256:70d56c784da1239c89d39fefa166fd429306dada641178389be4184a9c04e501",
"sha256:7de2c9e445a5d257935011268202338538abef1aaff341a4733eca56419ca6f6",
"sha256:96bb80b659cc2bafa160f3f0c346ce7fc10de1ffec4908d7f9690797f155f658",
"sha256:ae7501cc6a6c37b8d4774bf2218c37be47fe42019a2570e8510fc2044e59d573",
"sha256:c833aa6f4c9ac3e3eb843e3d999bae51339ad33a937303f43ce78064e61cb4b6",
"sha256:dd81d85a342edf3d2a388e2f24d9facebc9c04550043888f970ee2f228c93059",
"sha256:f20deec7a3fbaec7b5eb7ad99878427ad2ee4cc16a46732b705e8121cbb3cc12",
"sha256:f52e7287eb9286a1e91e4c67c207c2573147fbaddc68f70efb5aeee5d1992f2e",
"sha256:fe7b2972ff7e779e812f974aa5695edc328ecf559ceeea887ac46f06f090ad4c",
"sha256:ff1447c84a02b9cd5dd3a9332d1fb181a4386c3625765bb5caf1cfbc210ab3f9"
],
"version": "==3.3.2"
},
"aiohttp-json-rpc": {
"hashes": [
"sha256:bf1eb7e30949b60f74cb84731b5676bd7dc3f0298056ddbbe989d9219260008c",
"sha256:e1ae47d522a7857c612be8ba447cec3cad8c8b7d628353289a0889a1135166c8"
],
"version": "==0.11"
},
"alabaster": { "alabaster": {
"hashes": [ "hashes": [
"sha256:2eef172f44e8d301d25aff8068fddd65f767a3f04b5f15b0f4922f113aa1c732", "sha256:674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456",
"sha256:37cdcb9e9954ed60912ebc1ca12a9d12178c26637abdf124e3cde2341c257fe0" "sha256:b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7"
], ],
"version": "==0.7.10" "version": "==0.7.11"
}, },
"appdirs": { "appdirs": {
"hashes": [ "hashes": [
@@ -232,6 +305,14 @@
], ],
"version": "==1.4.3" "version": "==1.4.3"
}, },
"async-timeout": {
"hashes": [
"sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c",
"sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287"
],
"markers": "python_version >= '3.5.3'",
"version": "==3.0.0"
},
"atomicwrites": { "atomicwrites": {
"hashes": [ "hashes": [
"sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585", "sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585",
@@ -248,19 +329,17 @@
}, },
"babel": { "babel": {
"hashes": [ "hashes": [
"sha256:8ce4cb6fdd4393edd323227cba3a077bceb2a6ce5201c902c65e730046f41f14", "sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
"sha256:ad209a68d7162c4cff4b29cdebe3dec4cef75492df501b0049a9433c96ce6f80" "sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
], ],
"version": "==2.5.3" "version": "==2.6.0"
}, },
"black": { "black": {
"hashes": [ "hashes": [
"sha256:4fec2566f9fbbd4a58de50a168cbe3ab952713530410d227e82e4c65d1fad946", "sha256:22158b89c1a6b4eb333a1e65e791a3f8b998cf3b11ae094adb2570f31f769a44",
"sha256:5fec0f25486046b9edb97961c946412ced96021247dd1a60ecd9f0567b68b030" "sha256:4b475bbd528acce094c503a3d2dbc2d05a4075f6d0ef7d9e7514518e14cc5191"
], ],
"index": "pypi", "version": "==18.6b4"
"markers": "python_version >= '3.6'",
"version": "==18.5b0"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
@@ -283,6 +362,20 @@
], ],
"version": "==6.7" "version": "==6.7"
}, },
"colorama": {
"hashes": [
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
],
"version": "==0.3.9"
},
"distro": {
"hashes": [
"sha256:224041cef9600e72d19ae41ba006e71c05c4dc802516da715d7fda55ba3d8742",
"sha256:6ec8e539cf412830e5ccf521aecf879f2c7fcf60ce446e33cd16eef1ed8a0158"
],
"version": "==1.3.0"
},
"docutils": { "docutils": {
"hashes": [ "hashes": [
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
@@ -291,12 +384,34 @@
], ],
"version": "==0.14" "version": "==0.14"
}, },
"e1839a9": {
"editable": true,
"extras": [
"docs",
"test",
"style"
],
"path": "."
},
"fuzzywuzzy": {
"hashes": [
"sha256:d40c22d2744dff84885b30bbfc07fab7875f641d070374331777a4d1808b8d4e",
"sha256:ecf490216fb4d76b558a03042ff8f45a8782f17326caca1384d834cbaa2c7e6f"
],
"version": "==0.16.0"
},
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4" "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
], ],
"version": "==2.6" "version": "==2.7"
},
"idna-ssl": {
"hashes": [
"sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
],
"version": "==1.1.0"
}, },
"imagesize": { "imagesize": {
"hashes": [ "hashes": [
@@ -320,11 +435,30 @@
}, },
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [
"sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8", "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
"sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3", "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
"sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0" "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
], ],
"version": "==4.2.0" "version": "==4.3.0"
},
"multidict": {
"hashes": [
"sha256:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0",
"sha256:1d6e191965505652f194bc4c40270a842922685918a4f45e6936a6b15cc5816d",
"sha256:295961a6a88f1199e19968e15d9b42f3a191c89ec13034dbc212bf9c394c3c82",
"sha256:2be5af084de6c3b8e20d6421cb0346378a9c867dcf7c86030d6b0b550f9888e4",
"sha256:2eb99617c7a0e9f2b90b64bc1fb742611718618572747d6f3d6532b7b78755ab",
"sha256:4ba654c6b5ad1ae4a4d792abeb695b29ce981bb0f157a41d0fd227b385f2bef0",
"sha256:5ba766433c30d703f6b2c17eb0b6826c6f898e5f58d89373e235f07764952314",
"sha256:a59d58ee85b11f337b54933e8d758b2356fcdcc493248e004c9c5e5d11eedbe4",
"sha256:a6e35d28900cf87bcc11e6ca9e474db0099b78f0be0a41d95bef02d49101b5b2",
"sha256:b4df7ca9c01018a51e43937eaa41f2f5dce17a6382fda0086403bcb1f5c2cf8e",
"sha256:bbd5a6bffd3ba8bfe75b16b5e28af15265538e8be011b0b9fddc7d86a453fd4a",
"sha256:d870f399fcd58a1889e93008762a3b9a27cf7ea512818fc6e689f59495648355",
"sha256:e9404e2e19e901121c3c5c6cffd5a8ae0d1d67919c970e3b3262231175713068"
],
"markers": "python_version >= '3.4.1'",
"version": "==4.3.1"
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
@@ -335,18 +469,19 @@
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
"sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
"sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5"
], ],
"version": "==0.6.0" "markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
"version": "==0.7.1"
}, },
"py": { "py": {
"hashes": [ "hashes": [
"sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881", "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
"sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a" "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
], ],
"version": "==1.5.3" "markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
"version": "==1.5.4"
}, },
"pygments": { "pygments": {
"hashes": [ "hashes": [
@@ -358,44 +493,74 @@
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
"sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04", "sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04",
"sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07",
"sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18",
"sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e",
"sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5",
"sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58",
"sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010" "sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010"
], ],
"version": "==2.2.0" "version": "==2.2.0"
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:39555d023af3200d004d09e51b4dd9fdd828baa863cded3fd6ba2f29f757ae2d", "sha256:8214ab8446104a1d0c17fbd218ec6aac743236c6ffbe23abc038e40213c60b88",
"sha256:c76e93f3145a44812955e8d46cdd302d8a45fbfc7bf22be24fe231f9d8d8853a" "sha256:e2b2c6e1560b8f9dc8dd600b0923183fbd68ba3d9bdecde04467be6dd296a384"
], ],
"index": "pypi", "version": "==3.7.0"
"version": "==3.6.0"
}, },
"pytest-asyncio": { "pytest-asyncio": {
"hashes": [ "hashes": [
"sha256:286b50773e996c80d894b95afaf45df6952408a67a59979ca9839f94693ec7fd", "sha256:a962e8e1b6ec28648c8fe214edab4e16bacdb37b52df26eb9d63050af309b2a9",
"sha256:f32804bb58a66e13a3eda11f8942a71b1b6a30466b0d2ffe9214787aab0e172e" "sha256:fbd92c067c16111174a1286bfb253660f1e564e5146b39eeed1133315cf2c2cf"
], ],
"index": "pypi", "markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
"version": "==0.8.0" "version": "==0.9.0"
},
"python-levenshtein": {
"hashes": [
"sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1"
],
"version": "==0.12.0"
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555", "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
"sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749" "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
], ],
"version": "==2018.4" "version": "==2018.5"
},
"pyyaml": {
"hashes": [
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
],
"version": "==3.13"
},
"raven": {
"hashes": [
"sha256:3fd787d19ebb49919268f06f19310e8112d619ef364f7989246fc8753d469888",
"sha256:95f44f3ea2c1b176d5450df4becdb96c15bf2632888f9ab193e9dd22300ce46a"
],
"version": "==6.9.0"
},
"raven-aiohttp": {
"hashes": [
"sha256:1444a49c93a85b8bb57c6ee649e512368dce7a26ad64ac3a01d86aa5669d77f3",
"sha256:6a34b6a9841ad0fd827eeb158edb5826c5c5bd7babe2cde2a3f23eb85313af04"
],
"version": "==0.7.0"
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b", "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e" "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
], ],
"version": "==2.18.4" "version": "==2.19.1"
}, },
"six": { "six": {
"hashes": [ "hashes": [
@@ -413,55 +578,103 @@
}, },
"sphinx": { "sphinx": {
"hashes": [ "hashes": [
"sha256:2e7ad92e96eff1b2006cf9f0cdb2743dacbae63755458594e9e8238b0c3dc60b", "sha256:217ad9ece2156ed9f8af12b5d2c82a499ddf2c70a33c5f81864a08d8c67b9efc",
"sha256:e9b1a75a3eae05dded19c80eb17325be675e0698975baae976df603b6ed1eb10" "sha256:a765c6db1e5b62aae857697cd4402a5c1a315a7b0854bbcd0fc8cdc524da5896"
], ],
"index": "pypi", "version": "==1.7.6"
"version": "==1.7.4"
}, },
"sphinx-rtd-theme": { "sphinx-rtd-theme": {
"hashes": [ "hashes": [
"sha256:32424dac2779f0840b4788fbccb032ba2496c1ca47a439ad2510c8b1e55dfd33", "sha256:3b49758a64f8a1ebd8a33cb6cc9093c3935a908b716edfaa5772fd86aac27ef6",
"sha256:6d0481532b5f441b075127a2d755f430f1f8410a50112b1af6b069518548381d" "sha256:80e01ec0eb711abacb1fa507f3eae8b805ae8fa3e8b057abfdf497e3f644c82c"
], ],
"index": "pypi", "version": "==0.4.1"
"version": "==0.3.1"
}, },
"sphinxcontrib-asyncio": { "sphinxcontrib-asyncio": {
"hashes": [ "hashes": [
"sha256:96627b1ec4eba08d09ad577ff9416c131910333ef37a2c82a2716e59646739f0" "sha256:96627b1ec4eba08d09ad577ff9416c131910333ef37a2c82a2716e59646739f0"
], ],
"index": "pypi",
"version": "==0.2.0" "version": "==0.2.0"
}, },
"sphinxcontrib-websupport": { "sphinxcontrib-websupport": {
"hashes": [ "hashes": [
"sha256:7a85961326aa3a400cd4ad3c816d70ed6f7c740acd7ce5d78cd0a67825072eb9", "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
"sha256:f4932e95869599b89bf4f80fc3989132d83c9faa5bf633e7b5e0c25dffb75da2" "sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
], ],
"version": "==1.0.1" "markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
"version": "==1.1.0"
},
"toml": {
"hashes": [
"sha256:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d"
],
"version": "==0.9.4"
}, },
"tox": { "tox": {
"hashes": [ "hashes": [
"sha256:96efa09710a3daeeb845561ebbe1497641d9cef2ee0aea30db6969058b2bda2f", "sha256:37cf240781b662fb790710c6998527e65ca6851eace84d1595ee71f7af4e85f7",
"sha256:9ee7de958a43806402a38c0d2aa07fa8553f4d2c20a15b140e9f771c2afeade0" "sha256:eb61aa5bcce65325538686f09848f04ef679b5cd9b83cc491272099b28739600"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.0.0" "version": "==3.2.1"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
], ],
"version": "==1.22" "markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version < '4' and python_version >= '2.6' and python_version != '3.1.*'",
"version": "==1.23"
}, },
"virtualenv": { "virtualenv": {
"hashes": [ "hashes": [
"sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669", "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669",
"sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752" "sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752"
], ],
"markers": "python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*'",
"version": "==16.0.0" "version": "==16.0.0"
},
"websockets": {
"hashes": [
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
"sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6",
"sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1",
"sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538",
"sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4",
"sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908",
"sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0",
"sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d",
"sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c",
"sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d",
"sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c",
"sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb",
"sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf",
"sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e",
"sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96",
"sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584",
"sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484",
"sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d",
"sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559",
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
],
"markers": "python_version >= '3.4'",
"version": "==6.0"
},
"yarl": {
"hashes": [
"sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9",
"sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee",
"sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308",
"sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357",
"sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78",
"sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8",
"sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1",
"sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4",
"sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7"
],
"markers": "python_version >= '3.4.1'",
"version": "==1.2.6"
} }
} }
} }

View File

@@ -1,46 +1,47 @@
.. raw:: html .. class:: center
<h1 align="center"> .. image:: https://imgur.com/pY1WUFX.png
<br> :target: https://github.com/Cog-Creators/Red-DiscordBot/tree/V3/develop
<a href="https://github.com/Cog-Creators/Red-DiscordBot/tree/V3/develop"><img src="https://imgur.com/pY1WUFX.png" alt="Red Discord Bot"></a> :alt: Red Discord Bot
<br>
Red Discord Bot
<br>
</h1>
.. raw:: html
<h4 align="center">Music, Moderation, Trivia, Stream Alerts and fully customizable.</h4> .. class:: center
.. raw:: html Music, Moderation, Trivia, Stream Alerts and fully customizable.
<p align="center"> .. class:: center
<a href="https://discord.gg/red">
<img src="https://discordapp.com/api/guilds/133049272517001216/widget.png?style=shield">
</a>
<a href="https://www.patreon.com/Red_Devs">
<img src="https://img.shields.io/badge/Support-Red!-yellow.svg">
</a>
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/Made%20With-Python%203.6-blue.svg?style=for-the-badge">
</a>
<a href="https://crowdin.com/project/red-discordbot">
<img src="https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg">
</a>
<a href="https://github.com/Rapptz/discord.py/tree/rewrite">
<img src="https://img.shields.io/badge/discord-py-blue.svg">
</a>
</p>
.. raw:: html .. image:: https://discordapp.com/api/guilds/133049272517001216/embed.png
:target: https://discord.gg/red
:alt: Discord server
<p align="center"> .. image:: https://api.travis-ci.org/Cog-Creators/Red-DiscordBot.svg?branch=V3/develop
<a href="#overview">Overview</a> • :target: https://travis-ci.org/Cog-Creators/Red-DiscordBot
<a href="#installation">Installation</a> • :alt: Travis CI status
<a href="http://red-discordbot.readthedocs.io/en/v3-develop/index.html">Documentation</a>
<a href="#plugins"></a> • .. image:: https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop
<a href="#join-the-community">Community</a> • :target: http://red-discordbot.readthedocs.io/en/v3-develop/?badge=v3-develop
<a href="#license">License</a> :alt: Documentation Status
</p>
.. image:: https://img.shields.io/badge/discord-py-blue.svg
:target: https://github.com/Rapptz/discord.py
:alt: discord.py
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
:alt: Code style: black
.. image:: https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg
:target: https://crowdin.com/project/red-discordbot
:alt: Crowdin
.. image:: https://img.shields.io/badge/Support-Red!-orange.svg
:target: https://www.patreon.com/Red_Devs
:alt: Patreon
.. image:: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
:target: http://makeapullrequest.com
:alt: PRs open
========== ==========
Overview Overview

View File

@@ -1,5 +1,5 @@
api_key_env: CROWDIN_API_KEY api_key_env: CROWDIN_API_KEY
project_identifier_env: CROWDIN_PROJECT_ID project_identifier_env: CROWDIN_PROJECT_ID
files: files:
- source: /**/*.pot - source: /redbot/**/*.pot
translation: /%original_path%/%locale%.po translation: /%original_path%/%locale%.po

1
dependency_links.txt Normal file
View File

@@ -0,0 +1 @@
https://github.com/Rapptz/discord.py/tarball/8ccb98d395537b1c9acc187e1647dfdd07bb831b#egg=discord.py-1.0.0a0

View File

@@ -10,7 +10,7 @@ Creating the service file
Create the new service file: Create the new service file:
:code:`sudo nano /etc/systemd/system/red@.service` :code:`sudo -e /etc/systemd/system/red@.service`
Paste the following and replace all instances of :code:`username` with the username your bot is running under (hopefully not root): Paste the following and replace all instances of :code:`username` with the username your bot is running under (hopefully not root):

View File

@@ -37,6 +37,7 @@ For each of those, settings have varying priorities (listed below, highest to lo
7. Role settings (see below) 7. Role settings (see below)
8. Server whitelist 8. Server whitelist
9. Server blacklist 9. Server blacklist
10. Default settings
For the role whitelist and blacklist settings, For the role whitelist and blacklist settings,
roles will be checked individually in order from highest to lowest role the user has roles will be checked individually in order from highest to lowest role the user has
@@ -73,3 +74,34 @@ An example of the expected format is shown below.
- 96733288462286848 - 96733288462286848
default: allow default: allow
----------------------
Example configurations
----------------------
Locking Audio cog to approved server(s) as a bot owner
.. code-block:: none
[p]permissions setglobaldefault Audio deny
[p]permissions addglobalrule allow Audio [server ID or name]
Locking Audio to specific voice channel(s) as a serverowner or admin:
.. code-block:: none
[p]permissions setguilddefault deny play
[p]permissions setguilddefault deny "playlist start"
[p]permissions addguildrule allow play [voice channel ID or name]
[p]permissions addguildrule allow "playlist start" [voice channel ID or name]
Allowing extra roles to use cleanup
.. code-block:: none
[p]permissions addguildrule allow Cleanup [role ID]
Preventing cleanup from being used in channels where message history is important:
.. code-block:: none
[p]permissions addguildrule deny Cleanup [channel ID or mention]

View File

@@ -190,6 +190,13 @@ texinfo_documents = [
] ]
# -- Options for linkcheck builder ----------------------------------------
# A list of regular expressions that match URIs that should not be
# checked when doing a linkcheck build.
linkcheck_ignore = [r"https://java.com*"]
# Example configuration for intersphinx: refer to the Python standard library. # Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = { intersphinx_mapping = {
"python": ("https://docs.python.org/3.6", None), "python": ("https://docs.python.org/3.6", None),

View File

@@ -13,6 +13,9 @@ RedBase
:members: :members:
:exclude-members: get_context :exclude-members: get_context
.. automethod:: register_rpc_handler
.. automethod:: unregister_rpc_handler
Red Red
^^^ ^^^

View File

@@ -4,8 +4,10 @@
Commands Package Commands Package
================ ================
This package acts almost identically to ``discord.ext.commands``; i.e. they both have the same This package acts almost identically to :doc:`discord.ext.commands <dpy:ext/commands/api>`; i.e.
attributes. Some of these attributes, however, have been slightly modified, as outlined below. all of the attributes from discord.py's are also in ours.
Some of these attributes, however, have been slightly modified, while others have been added to
extend functionlities used throughout the bot, as outlined below.
.. autofunction:: redbot.core.commands.command .. autofunction:: redbot.core.commands.command

View File

@@ -21,6 +21,9 @@ Keys common to both repo and cog info.json (case sensitive)
- ``install_msg`` (string) - The message that gets displayed when a cog - ``install_msg`` (string) - The message that gets displayed when a cog
is installed or a repo is added is installed or a repo is added
.. tip:: You can use the ``[p]`` key in your string to use the prefix
used for installing.
- ``short`` (string) - A short description of the cog or repo. For cogs, this info - ``short`` (string) - A short description of the cog or repo. For cogs, this info
is displayed when a user executes ``!cog list`` is displayed when a user executes ``!cog list``
@@ -29,7 +32,9 @@ Keys specific to the cog info.json (case sensitive)
- ``bot_version`` (list of integer) - Min version number of Red in the format ``(MAJOR, MINOR, PATCH)`` - ``bot_version`` (list of integer) - Min version number of Red in the format ``(MAJOR, MINOR, PATCH)``
- ``hidden`` (bool) - Determines if a cog is available for install. - ``hidden`` (bool) - Determines if a cog is visible in the cog list for a repo.
- ``disabled`` (bool) - Determines if a cog is available for install.
- ``required_cogs`` (map of cogname to repo URL) - A map of required cogs that this cog depends on. - ``required_cogs`` (map of cogname to repo URL) - A map of required cogs that this cog depends on.
Downloader will not deal with this functionality but it may be useful for other cogs. Downloader will not deal with this functionality but it may be useful for other cogs.

View File

@@ -4,36 +4,60 @@
RPC RPC
=== ===
.. currentmodule:: redbot.core.rpc
V3 comes default with an internal RPC server that may be used to remotely control the bot in various ways. V3 comes default with an internal RPC server that may be used to remotely control the bot in various ways.
Cogs must register functions to be exposed to RPC clients. Cogs must register functions to be exposed to RPC clients.
Each of those functions must only take JSON serializable parameters and must return JSON serializable objects. Each of those functions must only take JSON serializable parameters and must return JSON serializable objects.
To begin, register all methods using individual calls to the :func:`Methods.add` method. To enable the internal RPC server you must start the bot with the ``--rpc`` flag.
******** ********
Examples Examples
******** ********
Coming soon to a docs page near you! .. code-block:: Python
def setup(bot):
c = Cog()
bot.add_cog(c)
bot.register_rpc_handler(c.rpc_method)
*******************************
Interacting with the RPC Server
*******************************
The RPC server opens a websocket bound to port ``6133`` on ``127.0.0.1``.
This is not configurable for security reasons as broad access to this server gives anyone complete control over your bot.
To access the server you must find a library that implements websocket based JSONRPC in the language of your choice.
There are a few built-in RPC methods to note:
* ``GET_METHODS`` - Returns a list of available RPC methods.
* ``GET_METHOD_INFO`` - Will return the docstring for an available RPC method. Useful for finding information about the method's parameters and return values.
* ``GET_TOPIC`` - Returns a list of available RPC message topics.
* ``GET_SUBSCRIPTIONS`` - Returns a list of RPC subscriptions.
* ``SUBSCRIBE`` - Subscribes to an available RPC message topic.
* ``UNSUBSCRIBE`` - Unsubscribes from an RPC message topic.
All RPC methods accept a list of parameters.
The built-in methods above expect their parameters to be in list format.
All cog-based methods expect their parameter list to take one argument, a JSON object, in the following format::
params = [
{
"args": [], # A list of positional arguments
"kwargs": {}, # A dictionary of keyword arguments
}
]
# As an example, here's a call to "get_method_info"
rpc_call("GET_METHOD_INFO", ["get_methods",])
# And here's a call to "core__load"
rpc_call("CORE__LOAD", {"args": [["general", "economy", "downloader"],], "kwargs": {}})
************* *************
API Reference API Reference
************* *************
.. py:attribute:: redbot.core.rpc.methods Please see the :class:`redbot.core.bot.RedBase` class for details on the RPC handler register and unregister methods.
An instance of the :class:`Methods` class.
All attempts to register new RPC methods **MUST** use this object.
You should never create a new instance of the :class:`Methods` class!
RPC
^^^
.. autoclass:: redbot.core.rpc.RPC
:members:
Methods
^^^^^^^
.. autoclass:: redbot.core.rpc.Methods
:members:

View File

@@ -4,6 +4,12 @@
Utility Functions Utility Functions
================= =================
General Utility
===============
.. automodule:: redbot.core.utils
:members: deduplicate_iterables, bounded_gather, bounded_gather_iter
Chat Formatting Chat Formatting
=============== ===============
@@ -39,3 +45,9 @@ Tunnel
.. automodule:: redbot.core.utils.tunnel .. automodule:: redbot.core.utils.tunnel
:members: Tunnel :members: Tunnel
Common Filters
==============
.. automodule:: redbot.core.utils.common_filters
:members:

View File

@@ -17,8 +17,7 @@ you in the process.
Getting started Getting started
--------------- ---------------
To start off, be sure that you have installed Python 3.5 or higher (if you To start off, be sure that you have installed Python 3.6 or higher. Open a terminal or command prompt and type
are on Windows, stick with 3.5). 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]` :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.

View File

@@ -11,13 +11,8 @@ Welcome to Red - Discord Bot's documentation!
:caption: Installation Guides: :caption: Installation Guides:
install_windows install_windows
install_mac install_linux_mac
install_ubuntu_xenial venv_guide
install_ubuntu_bionic
install_debian
install_centos
install_arch
install_raspbian
cog_dataconverter cog_dataconverter
autostart_systemd autostart_systemd

View File

@@ -1,55 +0,0 @@
.. arch install guide
==============================
Installing Red on Arch Linux
==============================
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, make a new one.
:code:`https://wiki.archlinux.org/index.php/Users_and_groups`
-------------------------------
Installing the pre-requirements
-------------------------------
.. code-block:: none
sudo pacman -Syu python-pip git base-devel jre8-openjdk
------------------
Installing the bot
------------------
To install without audio:
:code:`pip3 install -U --process-dependency-links red-discordbot --user`
To install with audio:
:code:`pip3 install -U --process-dependency-links red-discordbot[voice] --user`
To install the development version (without audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot --user`
To install the development version (with audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice] --user`
------------------------
Setting up your instance
------------------------
Run :code:`redbot-setup` and follow the prompts. It will ask first for where you want to
store the data (the default is :code:`~/.local/share/Red-DiscordBot`) and will then ask
for confirmation of that selection. Next, it will ask you to choose your storage backend
(the default here is JSON). It will then ask for a name for your instance. This can be
anything as long as it does not contain spaces; however, keep in mind that this is the
name you will use to run your bot, and so it should be something you can remember.
-----------
Running Red
-----------
Run :code:`redbot <your instance name>` and run through the initial setup. This will ask for
your token and a prefix.

View File

@@ -1,55 +0,0 @@
.. centos install guide
==========================
Installing Red on CentOS 7
==========================
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/4/html/Step_by_Step_Guide/s1-starting-create-account.html>`_.
---------------------------
Installing pre-requirements
---------------------------
.. code-block:: none
yum -y groupinstall development
yum -y install https://centos7.iuscommunity.org/ius-release.rpm
yum -y install yum-utils wget which python36u python36u-pip python36u-devel openssl-devel libffi-devel git java-1.8.0-openjdk
--------------
Installing Red
--------------
Without audio:
:code:`pip3 install -U --process-dependency-links red-discordbot --user`
With audio:
:code:`pip3 install -U --process-dependency-links red-discordbot[voice] --user`
To install the development version (without audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot --user`
To install the development version (with audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice] --user`
----------------------
Setting up an instance
----------------------
Run :code:`redbot-setup` and follow the prompts. It will ask first for where you want to
store the data (the default is :code:`~/.local/share/Red-DiscordBot`) and will then ask
for confirmation of that selection. Next, it will ask you to choose your storage backend
(the default here is JSON). It will then ask for a name for your instance. This can be
anything as long as it does not contain spaces; however, keep in mind that this is the
name you will use to run your bot, and so it should be something you can remember.
-----------
Running Red
-----------
Run :code:`redbot <your instance name>` and run through the initial setup. This will ask for
your token and a prefix.

View File

@@ -1,70 +0,0 @@
.. debian install guide
================================
Installing Red on Debian Stretch
================================
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <https://manpages.debian.org/stretch/adduser/adduser.8.en.html>`_.
---------------------------
Installing pre-requirements
---------------------------
.. code-block:: none
sudo apt install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev git unzip default-jre
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
After that last command, you may see a warning about 'pyenv' not being in the load path. Follow the instructions given to fix that, then close and reopen your shell
Then run the following command:
.. code-block:: none
CONFIGURE_OPTS=--enable-optimizations pyenv install 3.6.5 -v
This may take a long time to complete.
After that is finished, run:
.. code-block:: none
pyenv global 3.6.5
------------------
Installing the bot
------------------
To install without audio:
:code:`pip3 install -U --process-dependency-links red-discordbot`
To install with audio:
:code:`pip3 install -U --process-dependency-links red-discordbot[voice]`
To install the development version (without audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot`
To install the development version (with audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice]`
------------------------
Setting up your instance
------------------------
Run :code:`redbot-setup` and follow the prompts. It will ask first for where you want to
store the data (the default is :code:`~/.local/share/Red-DiscordBot`) and will then ask
for confirmation of that selection. Next, it will ask you to choose your storage backend
(the default here is JSON). It will then ask for a name for your instance. This can be
anything as long as it does not contain spaces; however, keep in mind that this is the
name you will use to run your bot, and so it should be something you can remember.
-----------
Running Red
-----------
Run :code:`redbot <your instance name>` and run through the initial setup. This will ask for
your token and a prefix.

203
docs/install_linux_mac.rst Normal file
View File

@@ -0,0 +1,203 @@
.. _linux-mac-install-guide:
==============================
Installing Red on Linux or Mac
==============================
.. warning::
For safety reasons, DO NOT install Red with a root user. If you are unsure how to create
a new user, see the man page for the ``useradd`` command.
-------------------------------
Installing the pre-requirements
-------------------------------
Please install the pre-requirements using the commands listed for your operating system.
The pre-requirements are:
- Python 3.6 or greater
- pip 9.0 or greater
- git
- Java Runtime Environment 8 or later (for audio support)
~~~~~~~~~~
Arch Linux
~~~~~~~~~~
.. code-block:: none
sudo pacman -Syu python-pip git base-devel jre8-openjdk
~~~~~~~~
CentOS 7
~~~~~~~~
.. code-block:: none
yum -y groupinstall development
yum -y install https://centos7.iuscommunity.org/ius-release.rpm
yum -y install yum-utils wget which python36u python36u-pip python36u-devel openssl-devel libffi-devel git java-1.8.0-openjdk
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Debian and Raspbian Stretch
~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. warning::
Audio will not work on Raspberry Pi's **below** 2B. This is a CPU problem and
*cannot* be fixed.
We recommend installing pyenv as a method of installing non-native versions of python on
Debian/Raspbian Stretch. This guide will tell you how. First, run the following commands:
.. code-block:: none
sudo apt install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev git unzip default-jre
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
After that last command, you may see a warning about 'pyenv' not being in the load path. Follow the
instructions given to fix that, then close and reopen your shell.
Then run the following command:
.. code-block:: none
CONFIGURE_OPTS=--enable-optimizations pyenv install 3.7.0 -v
This may take a long time to complete.
After that is finished, run:
.. code-block:: none
pyenv global 3.7.0
Pyenv is now installed and your system should be configured to run Python 3.7.
~~~
Mac
~~~
Install Brew: in Finder or Spotlight, search for and open *Terminal*. In the terminal, paste the
following, then press Enter:
.. code-block:: none
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
After the installation, install the required packages by pasting the commands and pressing enter,
one-by-one:
.. code-block:: none
brew install python3 --with-brewed-openssl
brew install git
brew tap caskroom/versions
brew cask install java8
~~~~~~~~~~~~~~~~~~~~~~~~~~
Ubuntu 18.04 Bionic Beaver
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: none
sudo apt install python3.6-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
~~~~~~~~~~~~~~~~~~~~~~~~~
Ubuntu 16.04 Xenial Xerus
~~~~~~~~~~~~~~~~~~~~~~~~~
We recommend adding the ``deadsnakes`` apt repository to install Python 3.6 or greater:
.. code-block:: none
sudo apt install software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
Now, install python, pip, git and java with the following commands:
.. code-block:: none
sudo apt install python3.6-dev build-essential libssl-dev libffi-dev git unzip default-jre wget -y
wget https://bootstrap.pypa.io/get-pip.py
sudo python3.6 get-pip.py
------------------------------
Creating a Virtual Environment
------------------------------
We **strongly** recommend installing Red into a virtual environment. See the section
`installing-in-virtual-environment`.
.. _installing-red-linux-mac:
--------------
Installing Red
--------------
Choose one of the following commands to install Red.
.. note::
If you're not inside an activated virtual environment, include the ``--user`` flag with all
``pip3`` commands.
To install without audio support:
.. code-block:: none
pip3 install -U --process-dependency-links --no-cache-dir Red-DiscordBot
Or, to install with audio support:
.. code-block:: none
pip3 install -U --process-dependency-links --no-cache-dir Red-DiscordBot[voice]
Or, install with audio and MongoDB support:
.. code-block:: none
pip3 install -U --process-dependency-links --no-cache-dir Red-DiscordBot[voice,mongo]
.. note::
To install the development version, replace ``Red-DiscordBot`` in the above commands with the
following link:
.. code-block:: none
git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=Red-DiscordBot
--------------------------
Setting Up and Running Red
--------------------------
After installation, set up your instance with the following command:
.. code-block:: none
redbot-setup
This will set the location where data will be stored, as well as your
storage backend and the name of the instance (which will be used for
running the bot).
Once done setting up the instance, run the following command to run Red:
.. code-block:: none
redbot <your instance name>
It will walk through the initial setup, asking for your token and a prefix.
You may also run Red via the launcher, which allows you to restart the bot
from discord, and enable auto-restart. You may also update the bot from the
launcher menu. Use the following command to run the launcher:
.. code-block:: none
redbot-launcher

View File

@@ -1,53 +0,0 @@
.. mac install guide
=====================
Installing Red on Mac
=====================
---------------------------
Installing pre-requirements
---------------------------
* Install Brew
* In Finder or Spotlight, search for and open terminal. In the window that will open, paste this:
:code:`/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"`
and press enter.
* After the installation, install the required packages by pasting the commands and pressing enter, one-by-one:
* :code:`brew install python3 --with-brewed-openssl`
* :code:`brew install git`
* :code:`brew tap caskroom/versions`
* :code:`brew cask install java8`
--------------
Installing Red
--------------
Without audio:
:code:`pip3 install -U --process-dependency-links red-discordbot`
With audio:
:code:`pip3 install -U --process-dependency-links red-discordbot[voice]`
To install the development version (without audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot`
To install the development version (with audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice]`
----------------------
Setting up an instance
----------------------
To set up an instance, run :code:`redbot-setup` and follow the steps there, providing the requested information
or accepting the defaults. Keep in mind that the instance name will be the one you use when running the bot, so
make it something you can remember
-----------
Running Red
-----------
Run :code:`redbot <your instance name>` and go through the initial setup (it will ask for the token and a prefix).

View File

@@ -1,72 +0,0 @@
.. raspbian install guide
==================================
Installing Red on Raspbian Stretch
==================================
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <https://www.raspberrypi.org/documentation/linux/usage/users.md>`_.
---------------------------
Installing pre-requirements
---------------------------
.. code-block:: none
sudo apt install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev git unzip default-jre
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
After that last command, you may see a warning about 'pyenv' not being in the load path. Follow the instructions given to fix that, then close and reopen your shell
Then run the following command:
.. code-block:: none
CONFIGURE_OPTS=--enable-optimizations pyenv install 3.6.5 -v
This may take a long time to complete.
After that is finished, run:
.. code-block:: none
pyenv global 3.6.5
--------------
Installing Red
--------------
Without audio:
:code:`pip3 install -U --process-dependency-links red-discordbot --user`
With audio:
:code:`pip3 install -U --process-dependency-links red-discordbot[voice] --user`
To install the development version (without audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot --user`
To install the development version (with audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice] --user`
----------------------
Setting up an instance
----------------------
Run :code:`redbot-setup` and follow the prompts. It will ask first for where you want to
store the data (the default is :code:`~/.local/share/Red-DiscordBot`) and will then ask
for confirmation of that selection. Next, it will ask you to choose your storage backend
(the default here is JSON). It will then ask for a name for your instance. This can be
anything as long as it does not contain spaces; however, keep in mind that this is the
name you will use to run your bot, and so it should be something you can remember.
-----------
Running Red
-----------
Run :code:`redbot <your instance name>` and run through the initial setup. This will ask for
your token and a prefix.
.. warning:: Audio will not work on Raspberry Pi's **below** 2B. This is a CPU problem and *cannot* be fixed.

View File

@@ -1,54 +0,0 @@
.. ubuntu bionic install guide
==============================
Installing Red on Ubuntu 18.04
==============================
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <http://manpages.ubuntu.com/manpages/artful/man8/adduser.8.html>`_.
-------------------------------
Installing the pre-requirements
-------------------------------
.. code-block:: none
sudo apt install python3.6-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
------------------
Installing the bot
------------------
To install without audio:
:code:`pip3 install -U --process-dependency-links red-discordbot --user`
To install with audio:
:code:`pip3 install -U --process-dependency-links red-discordbot[voice] --user`
To install the development version (without audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot --user`
To install the development version (with audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice] --user`
------------------------
Setting up your instance
------------------------
Run :code:`redbot-setup` and follow the prompts. It will ask first for where you want to
store the data (the default is :code:`~/.local/share/Red-DiscordBot`) and will then ask
for confirmation of that selection. Next, it will ask you to choose your storage backend
(the default here is JSON). It will then ask for a name for your instance. This can be
anything as long as it does not contain spaces; however, keep in mind that this is the
name you will use to run your bot, and so it should be something you can remember.
-----------
Running Red
-----------
Run :code:`redbot <your instance name>` and run through the initial setup. This will ask for
your token and a prefix.

View File

@@ -1,59 +0,0 @@
.. ubuntu xenial install guide
==============================
Installing Red on Ubuntu 16.04
==============================
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <http://manpages.ubuntu.com/manpages/artful/man8/adduser.8.html>`_.
-------------------------------
Installing the pre-requirements
-------------------------------
.. code-block:: none
sudo apt install software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install python3.6-dev build-essential libssl-dev libffi-dev git unzip default-jre wget -y
wget https://bootstrap.pypa.io/get-pip.py
sudo python3.6 get-pip.py
------------------
Installing the bot
------------------
To install without audio:
:code:`pip3.6 install -U --process-dependency-links red-discordbot --user`
To install with audio:
:code:`pip3.6 install -U --process-dependency-links red-discordbot[voice] --user`
To install the development version (without audio):
:code:`pip3.6 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot --user`
To install the development version (with audio):
:code:`pip3.6 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice] --user`
------------------------
Setting up your instance
------------------------
Run :code:`redbot-setup` and follow the prompts. It will ask first for where you want to
store the data (the default is :code:`~/.local/share/Red-DiscordBot`) and will then ask
for confirmation of that selection. Next, it will ask you to choose your storage backend
(the default here is JSON). It will then ask for a name for your instance. This can be
anything as long as it does not contain spaces; however, keep in mind that this is the
name you will use to run your bot, and so it should be something you can remember.
-----------
Running Red
-----------
Run :code:`redbot <your instance name>` and run through the initial setup. This will ask for
your token and a prefix.

View File

@@ -1,4 +1,4 @@
.. windows installation docs .. _windows-install-guide:
========================= =========================
Installing Red on Windows Installing Red on Windows
@@ -8,7 +8,7 @@ Installing Red on Windows
Needed Software Needed Software
--------------- ---------------
* `Python <https://python.org/downloads/>`_ - Red needs Python 3.6 * `Python <https://www.python.org/downloads/>`_ - Red needs Python 3.6
.. 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
@@ -21,23 +21,74 @@ Needed Software
.. attention:: Please choose the "Windows Online" installer .. attention:: Please choose the "Windows Online" installer
.. _installing-red-windows:
-------------- --------------
Installing Red Installing Red
-------------- --------------
1. Open a command prompt (open Start, search for "command prompt", then click it) 1. Open a command prompt (open Start, search for "command prompt", then click it)
2. Run the appropriate command, depending on if you want audio or not 2. Create and activate a virtual environment (strongly recommended), see the section `using-venv`
3. Run **one** of the following commands, depending on what extras you want installed
* No audio: :code:`python -m pip install -U --process-dependency-links Red-DiscordBot` .. note::
* Audio: :code:`python -m pip install -U --process-dependency-links Red-DiscordBot[voice]`
* Development version (without audio): :code:`python -m pip install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot`
* Development version (with audio): :code:`python -m pip install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice]`
3. Once that has completed, run :code:`redbot-setup` to set up your instance If you're not inside an activated virtual environment, include the ``--user`` flag with all
``pip`` commands.
* This will set the location where data will be stored, as well as your * No audio:
storage backend and the name of the instance (which will be used for
running the bot)
4. Once done setting up the instance, run :code:`redbot <your instance name>` to run Red. .. code-block:: none
It will walk through the initial setup, asking for your token and a prefix
python -m pip install -U --process-dependency-links --no-cache-dir Red-DiscordBot
* With audio:
.. code-block:: none
python -m pip install -U --process-dependency-links --no-cache-dir Red-DiscordBot[voice]
* With audio and MongoDB support:
.. code-block:: none
python -m pip install -U --process-dependency-links --no-cache-dir Red-DiscordBot[voice,mongo]
.. note::
To install the development version, replace ``Red-DiscordBot`` in the above commands with the
following link:
.. code-block:: none
git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=Red-DiscordBot
--------------------------
Setting Up and Running Red
--------------------------
After installation, set up your instance with the following command:
.. code-block:: none
redbot-setup
This will set the location where data will be stored, as well as your
storage backend and the name of the instance (which will be used for
running the bot).
Once done setting up the instance, run the following command to run Red:
.. code-block:: none
redbot <your instance name>
It will walk through the initial setup, asking for your token and a prefix.
You may also run Red via the launcher, which allows you to restart the bot
from discord, and enable auto-restart. You may also update the bot from the
launcher menu. Use the following command to run the launcher:
.. code-block:: none
redbot-launcher

View File

@@ -1,29 +0,0 @@
-i https://pypi.org/simple
alabaster==0.7.10
attrs==18.1.0
babel==2.5.3
certifi==2018.4.16
chardet==3.0.4
docutils==0.14
idna==2.6
imagesize==1.0.0
jinja2==2.10
markupsafe==1.0
more-itertools==4.1.0
packaging==17.1
pluggy==0.6.0
py==1.5.3
pygments==2.2.0
pyparsing==2.2.0
pytest-asyncio==0.8.0
pytest==3.5.1
pytz==2018.4
requests==2.18.4
six==1.11.0
snowballstemmer==1.2.1
sphinx-rtd-theme==0.3.1
sphinx==1.7.4
sphinxcontrib-asyncio==0.2.0
sphinxcontrib-websupport==1.0.1
urllib3==1.22
git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py-1.0

132
docs/venv_guide.rst Normal file
View File

@@ -0,0 +1,132 @@
.. _installing-in-virtual-environment:
=======================================
Installing Red in a Virtual Environment
=======================================
Virtual environments allow you to isolate red's library dependencies, cog dependencies and python
binaries from the rest of your system. It is strongly recommended you use this if you use python
for more than just Red.
.. _using-venv:
--------------
Using ``venv``
--------------
This is the quickest way to get your virtual environment up and running, as `venv` is shipped with
python.
First, choose a directory where you would like to create your virtual environment. It's a good idea
to keep it in a location which is easy to type out the path to. From now, we'll call it
``path/to/venv/`` (or ``path\to\venv\`` on Windows).
~~~~~~~~~~~~~~~~~~~~~~~~
``venv`` on Linux or Mac
~~~~~~~~~~~~~~~~~~~~~~~~
Create your virtual environment with the following command::
python3 -m venv path/to/venv/
And activate it with the following command::
source path/to/venv/bin/activate
.. important::
You must activate the virtual environment with the above command every time you open a new
shell to run, install or update Red.
Continue reading `below <after-activating-virtual-environment>`.
~~~~~~~~~~~~~~~~~~~
``venv`` on Windows
~~~~~~~~~~~~~~~~~~~
Create your virtual environment with the following command::
python -m venv path\to\venv\
And activate it with the following command::
path\to\venv\Scripts\activate.bat
.. important::
You must activate the virtual environment with the above command every time you open a new
Command Prompt to run, install or update Red.
Continue reading `below <after-activating-virtual-environment>`.
.. _using-pyenv-virtualenv:
--------------------------
Using ``pyenv virtualenv``
--------------------------
.. note::
This is for non-Windows users only.
Using ``pyenv virtualenv`` saves you the headache of remembering where you installed your virtual
environments. If you haven't already, install pyenv with `pyenv-installer`_.
First, ensure your pyenv interpreter is set to python 3.6 or later with the following command::
pyenv version
Now, create a virtual environment with the following command::
pyenv virtualenv <name>
Replace ``<name>`` with whatever you like. If you forget what you named it, use the command ``pyenv
versions``.
Now activate your virtualenv with the following command::
pyenv shell <name>
.. important::
You must activate the virtual environment with the above command every time you open a new
shell to run, install or update Red.
Continue reading `below <after-activating-virtual-environment>`.
.. _pyenv-installer: https://github.com/pyenv/pyenv-installer/blob/master/README.rst
----
.. _after-activating-virtual-environment:
Once activated, your ``PATH`` environment variable will be modified to use the virtual
environment's python executables, as well as other executables like ``pip``.
From here, install Red using the commands listed on your installation guide (`Windows
<installing-red-windows>` or `Non-Windows <installing-red-linux-mac>`).
.. note::
The alternative to activating the virtual environment each time you open a new shell is to
provide the full path to the executable. This will automatically use the virtual environment's
python interpreter and installed libraries.
--------------------------------------------
Virtual Environments with Multiple Instances
--------------------------------------------
If you are running multiple instances of Red on the same machine, you have the option of either
using the same virtual environment for all of them, or creating separate ones.
.. note::
This only applies for multiple instances of V3. If you are running a V2 instance as well,
You **must** use separate virtual environments.
The advantages of using a *single* virtual environment for all of your V3 instances are:
- When updating Red, you will only need to update it once for all instances (however you will still need to restart all instances for the changes to take effect)
- It will save space on your hard drive
On the other hand, you may wish to update each of your instances individually.
.. important::
Windows users with multiple instances should create *separate* virtual environments, as
updating multiple running instances at once is likely to cause errors.

View File

@@ -1,37 +0,0 @@
import subprocess
import os
import sys
def main():
interpreter = sys.executable
print(interpreter)
root_dir = os.getcwd()
cogs = [i for i in os.listdir("redbot/cogs") if os.path.isdir(os.path.join("redbot/cogs", i))]
for d in cogs:
if "locales" in os.listdir(os.path.join("redbot/cogs", d)):
os.chdir(os.path.join("redbot/cogs", d, "locales"))
if "regen_messages.py" not in os.listdir(os.getcwd()):
print(
"Directory 'locales' exists for {} but no 'regen_messages.py' is available!".format(
d
)
)
exit(1)
else:
print("Running 'regen_messages.py' for {}".format(d))
retval = subprocess.run([interpreter, "regen_messages.py"])
if retval.returncode != 0:
exit(1)
os.chdir(root_dir)
os.chdir("redbot/core/locales")
print("Running 'regen_messages.py' for core")
retval = subprocess.run([interpreter, "regen_messages.py"])
if retval.returncode != 0:
exit(1)
os.chdir(root_dir)
subprocess.run(["crowdin", "upload"])
if __name__ == "__main__":
main()

View File

@@ -12,12 +12,3 @@ if discord.version_info.major < 1:
" >= 1.0.0." " >= 1.0.0."
) )
sys.exit(1) sys.exit(1)
if sys.version_info < (3, 6, 0):
print(Back.RED + "[DEPRECATION WARNING]")
print(
Back.RED + "You are currently running Python 3.5."
" Support for Python 3.5 will end with the release of beta 16."
" Please update your environment to Python 3.6 as soon as possible to avoid"
" any interruptions after the beta 16 release."
)

View File

@@ -6,19 +6,28 @@ import sys
import discord import discord
from redbot.core.bot import Red, ExitCodes from redbot.core.bot import Red, ExitCodes
from redbot.core.cog_manager import CogManagerUI from redbot.core.cog_manager import CogManagerUI
from redbot.core.data_manager import load_basic_configuration, config_file from redbot.core.data_manager import create_temp_config, load_basic_configuration, config_file
from redbot.core.json_io import JsonIO from redbot.core.json_io import JsonIO
from redbot.core.global_checks import init_global_checks from redbot.core.global_checks import init_global_checks
from redbot.core.events import init_events from redbot.core.events import init_events
from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry
from redbot.core.core_commands import Core from redbot.core.core_commands import Core
from redbot.core.dev_commands import Dev from redbot.core.dev_commands import Dev
from redbot.core import rpc, __version__ from redbot.core import __version__
import asyncio import asyncio
import logging.handlers import logging.handlers
import logging import logging
import os import os
# Let's not force this dependency, uvloop is much faster on cpython
if sys.implementation.name == "cpython":
try:
import uvloop
except ImportError:
pass
else:
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
# #
# Red - Discord Bot v3 # Red - Discord Bot v3
@@ -40,7 +49,7 @@ def init_loggers(cli_flags):
logger = logging.getLogger("red") logger = logging.getLogger("red")
red_format = logging.Formatter( red_format = logging.Formatter(
"%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: " "%(message)s", "%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: %(message)s",
datefmt="[%d/%m/%Y %H:%M]", datefmt="[%d/%m/%Y %H:%M]",
) )
@@ -106,12 +115,20 @@ def main():
elif cli_flags.version: elif cli_flags.version:
print(description) print(description)
sys.exit(0) sys.exit(0)
elif not cli_flags.instance_name: elif not cli_flags.instance_name and not cli_flags.no_instance:
print("Error: No instance name was provided!") print("Error: No instance name was provided!")
sys.exit(1) sys.exit(1)
if cli_flags.no_instance:
print(
"\033[1m"
"Warning: The data will be placed in a temporary folder and removed on next system reboot."
"\033[0m"
)
cli_flags.instance_name = "temporary_red"
create_temp_config()
load_basic_configuration(cli_flags.instance_name) load_basic_configuration(cli_flags.instance_name)
log, sentry_log = init_loggers(cli_flags) log, sentry_log = init_loggers(cli_flags)
red = Red(cli_flags, description=description, pm_help=None) red = Red(cli_flags=cli_flags, description=description, pm_help=None)
init_global_checks(red) init_global_checks(red)
init_events(red, cli_flags) init_events(red, cli_flags)
red.add_cog(Core(red)) red.add_cog(Core(red))
@@ -122,8 +139,10 @@ def main():
tmp_data = {} tmp_data = {}
loop.run_until_complete(_get_prefix_and_token(red, tmp_data)) loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
token = os.environ.get("RED_TOKEN", tmp_data["token"]) token = os.environ.get("RED_TOKEN", tmp_data["token"])
if cli_flags.token:
token = cli_flags.token
prefix = cli_flags.prefix or tmp_data["prefix"] prefix = cli_flags.prefix or tmp_data["prefix"]
if token is None or not prefix: if not (token and prefix):
if cli_flags.no_prompt is False: if cli_flags.no_prompt is False:
new_token = interactive_config(red, token_set=bool(token), prefix_set=bool(prefix)) new_token = interactive_config(red, token_set=bool(token), prefix_set=bool(prefix))
if new_token: if new_token:
@@ -138,18 +157,11 @@ def main():
sys.exit(0) sys.exit(0)
if tmp_data["enable_sentry"]: if tmp_data["enable_sentry"]:
red.enable_sentry() red.enable_sentry()
cleanup_tasks = True
try: try:
loop.run_until_complete(red.start(token, bot=not cli_flags.not_bot)) loop.run_until_complete(red.start(token, bot=True))
except discord.LoginFailure: except discord.LoginFailure:
cleanup_tasks = False # No login happened, no need for this log.critical("This token doesn't seem to be valid.")
log.critical( db_token = loop.run_until_complete(red.db.token())
"This token doesn't seem to be valid. If it belongs to "
"a user account, remember that the --not-bot flag "
"must be used. For self-bot functionalities instead, "
"--self-bot"
)
db_token = red.db.token()
if db_token and not cli_flags.no_prompt: if db_token and not cli_flags.no_prompt:
print("\nDo you want to reset the token? (y/n)") print("\nDo you want to reset the token? (y/n)")
if confirm("> "): if confirm("> "):
@@ -164,10 +176,13 @@ def main():
sentry_log.critical("Fatal Exception", exc_info=e) sentry_log.critical("Fatal Exception", exc_info=e)
loop.run_until_complete(red.logout()) loop.run_until_complete(red.logout())
finally: finally:
if cleanup_tasks:
pending = asyncio.Task.all_tasks(loop=red.loop) pending = asyncio.Task.all_tasks(loop=red.loop)
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True) gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
gathered.cancel() gathered.cancel()
try:
red.rpc.server.close()
except AttributeError:
pass
sys.exit(red._shutdown_mode.value) sys.exit(red._shutdown_mode.value)

View File

@@ -1,9 +1,8 @@
from typing import Tuple from typing import Tuple
import discord import discord
from discord.ext import commands
from redbot.core import Config, checks from redbot.core import Config, checks, commands
import logging import logging
@@ -40,7 +39,6 @@ RUNNING_ANNOUNCEMENT = (
class Admin: class Admin:
def __init__(self, config=Config): def __init__(self, config=Config):
self.conf = config.get_conf(self, 8237492837454039, force_registration=True) self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
@@ -129,8 +127,8 @@ class Admin:
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
): ):
""" """
Adds a role to a user. If user is left blank it defaults to the Adds a role to a user.
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:
user = ctx.author user = ctx.author
@@ -138,7 +136,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) await self.complain(ctx, USER_HIERARCHY_ISSUE, member=ctx.author, role=rolename)
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@@ -147,8 +145,8 @@ class Admin:
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
): ):
""" """
Removes a role from a user. If user is left blank it defaults to the Removes a role from a user.
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:
user = ctx.author user = ctx.author
@@ -163,8 +161,7 @@ class Admin:
@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""" """Edits roles settings"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@editrole.command(name="colour", aliases=["color"]) @editrole.command(name="colour", aliases=["color"])
async def editrole_colour( async def editrole_colour(
@@ -265,20 +262,16 @@ class Admin:
@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, *, guild: discord.Guild = None): async def announce_ignore(self, ctx):
""" """
Toggles whether the announcements will ignore the given server. Toggles whether the announcements will ignore the current server.
Defaults to the current server if none is provided.
""" """
if guild is None: ignored = await self.conf.guild(ctx.guild).announce_ignore()
guild = ctx.guild await self.conf.guild(ctx.guild).announce_ignore.set(not ignored)
ignored = await self.conf.guild(guild).announce_ignore()
await self.conf.guild(guild).announce_ignore.set(not ignored)
verb = "will" if ignored else "will not" verb = "will" if ignored else "will not"
await ctx.send("The server {} {} receive announcements.".format(guild.name, verb)) await ctx.send(f"The server {ctx.guild.name} {verb} receive announcements.")
async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]: async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]:
""" """
@@ -298,11 +291,13 @@ class Admin:
# noinspection PyTypeChecker # noinspection PyTypeChecker
return valid_roles return valid_roles
@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 that server admins have configured as Add a role to yourself that server admins have configured as user settable.
user settable.
NOTE: The role is case sensitive!
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
await self._addrole(ctx, ctx.author, selfrole) await self._addrole(ctx, ctx.author, selfrole)
@@ -311,15 +306,19 @@ class Admin:
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole): async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
""" """
Removes a selfrole from yourself. Removes a selfrole from yourself.
NOTE: The role is case sensitive!
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
await self._removerole(ctx, ctx.author, selfrole) await self._removerole(ctx, ctx.author, selfrole)
@selfrole.command(name="add") @selfrole.command(name="add")
@commands.has_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!
""" """
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles: async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
if role.id not in curr_selfroles: if role.id not in curr_selfroles:
@@ -328,10 +327,12 @@ class Admin:
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")
@commands.has_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):
""" """
Removes a role from the list of available selfroles. Removes a role from the list of available selfroles.
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)

View File

@@ -1,11 +1,10 @@
import asyncio import asyncio
import discord import discord
from discord.ext import commands from redbot.core import commands
class Announcer: class Announcer:
def __init__(self, ctx: commands.Context, message: str, config=None): def __init__(self, ctx: commands.Context, message: str, config=None):
""" """
:param ctx: :param ctx:

View File

@@ -1,9 +1,8 @@
import discord import discord
from discord.ext import commands from redbot.core import commands
class MemberDefaultAuthor(commands.Converter): class MemberDefaultAuthor(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> discord.Member: async def convert(self, ctx: commands.Context, arg: str) -> discord.Member:
member_converter = commands.MemberConverter() member_converter = commands.MemberConverter()
try: try:
@@ -17,7 +16,6 @@ class MemberDefaultAuthor(commands.Converter):
class SelfRole(commands.Converter): class SelfRole(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> discord.Role: async def convert(self, ctx: commands.Context, arg: str) -> discord.Role:
admin = ctx.command.instance admin = ctx.command.instance
if admin is None: if admin is None:
@@ -30,5 +28,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

View File

@@ -1,17 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../admin.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

View File

@@ -1,6 +1,6 @@
from .alias import Alias from .alias import Alias
from discord.ext import commands from redbot.core.bot import Red
def setup(bot: commands.Bot): def setup(bot: Red):
bot.add_cog(Alias(bot)) bot.add_cog(Alias(bot))

View File

@@ -174,16 +174,14 @@ class Alias:
@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 per-server aliases for commands"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@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.
""" """
if ctx.invoked_subcommand is None or isinstance(ctx.invoked_subcommand, commands.Group): pass
await ctx.send_help()
@checks.mod_or_permissions(manage_guild=True) @checks.mod_or_permissions(manage_guild=True)
@alias.command(name="add") @alias.command(name="add")
@@ -233,9 +231,7 @@ class Alias:
await self.add_alias(ctx, alias_name, command) await self.add_alias(ctx, alias_name, command)
await ctx.send( await ctx.send(_("A new alias with the trigger `{}` has been created.").format(alias_name))
_("A new alias with the trigger `{}`" " has been created.").format(alias_name)
)
@checks.is_owner() @checks.is_owner()
@global_.command(name="add") @global_.command(name="add")
@@ -282,14 +278,14 @@ 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 `{}` has been created.").format(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""" """Tries to execute help for the base command of the alias"""
is_alias, alias = 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]
@@ -307,9 +303,7 @@ class Alias:
if is_alias: if is_alias:
await ctx.send( await ctx.send(
_("The `{}` alias will execute the" " command `{}`").format( _("The `{}` alias will execute the command `{}`").format(alias_name, alias.command)
alias_name, 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 `{}`").format(alias_name))
@@ -330,7 +324,7 @@ class Alias:
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 `{}` was successfully deleted.").format(alias_name)
) )
else: else:
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name)) await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
@@ -350,7 +344,7 @@ 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 `{}` was successfully deleted.").format(alias_name)
) )
else: else:
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name)) await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))

View File

@@ -5,7 +5,6 @@ from redbot.core import commands
class AliasEntry: class AliasEntry:
def __init__( def __init__(
self, name: str, command: Tuple[str], creator: discord.Member, global_: bool = False self, name: str, command: Tuple[str], creator: discord.Member, global_: bool = False
): ):

View File

@@ -1,89 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: ../alias.py:129
msgid "No prefix found."
msgstr ""
#: ../alias.py:198
msgid "You attempted to create a new alias with the name {} but that name is already a command on this bot."
msgstr ""
#: ../alias.py:205
msgid "You attempted to create a new alias with the name {} but that alias already exists on this server."
msgstr ""
#: ../alias.py:212
msgid "You attempted to create a new alias with the name {} but that name is an invalid alias name. Alias names may not contain spaces."
msgstr ""
#: ../alias.py:224
msgid "A new alias with the trigger `{}` has been created."
msgstr ""
#: ../alias.py:236
msgid "You attempted to create a new global alias with the name {} but that name is already a command on this bot."
msgstr ""
#: ../alias.py:243
msgid "You attempted to create a new global alias with the name {} but that alias already exists on this server."
msgstr ""
#: ../alias.py:250
msgid "You attempted to create a new global alias with the name {} but that name is an invalid alias name. Alias names may not contain spaces."
msgstr ""
#: ../alias.py:259
msgid "A new global alias with the trigger `{}` has been created."
msgstr ""
#: ../alias.py:274
msgid "No such alias exists."
msgstr ""
#: ../alias.py:283
msgid "The `{}` alias will execute the command `{}`"
msgstr ""
#: ../alias.py:286
msgid "There is no alias with the name `{}`"
msgstr ""
#: ../alias.py:298
msgid "There are no aliases on this guild."
msgstr ""
#: ../alias.py:302 ../alias.py:320
msgid "Alias with the name `{}` was successfully deleted."
msgstr ""
#: ../alias.py:305 ../alias.py:323
msgid "Alias with name `{}` was not found."
msgstr ""
#: ../alias.py:316
msgid "There are no aliases on this bot."
msgstr ""
#: ../alias.py:331 ../alias.py:342
msgid "Aliases:"
msgstr ""
#: ../alias.py:333 ../alias.py:344
msgid "There are no aliases on this server."
msgstr ""

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../alias.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

View File

@@ -1,22 +1,25 @@
from pathlib import Path from pathlib import Path
from aiohttp import ClientSession from aiohttp import ClientSession
import shutil import shutil
import logging
from .audio import Audio from .audio import Audio
from .manager import start_lavalink_server from .manager import start_lavalink_server
from discord.ext import commands from redbot.core import commands
from redbot.core.data_manager import cog_data_path from redbot.core.data_manager import cog_data_path
import redbot.core import redbot.core
log = logging.getLogger("red.audio")
LAVALINK_DOWNLOAD_URL = ( LAVALINK_DOWNLOAD_URL = (
"https://github.com/Cog-Creators/Red-DiscordBot/" "releases/download/{}/Lavalink.jar" "https://github.com/Cog-Creators/Red-DiscordBot/releases/download/{}/Lavalink.jar"
).format(redbot.core.__version__) ).format(redbot.core.__version__)
LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio") LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio")
LAVALINK_JAR_FILE = LAVALINK_DOWNLOAD_DIR / "Lavalink.jar" LAVALINK_JAR_FILE = LAVALINK_DOWNLOAD_DIR / "Lavalink.jar"
APP_YML_FILE = LAVALINK_DOWNLOAD_DIR / "application.yml" APP_YML_FILE = LAVALINK_DOWNLOAD_DIR / "application.yml"
BUNDLED_APP_YML_FILE = Path(__file__).parent / "application.yml" BUNDLED_APP_YML_FILE = Path(__file__).parent / "data/application.yml"
async def download_lavalink(session): async def download_lavalink(session):
@@ -33,15 +36,13 @@ async def maybe_download_lavalink(loop, cog):
jar_exists = LAVALINK_JAR_FILE.exists() jar_exists = LAVALINK_JAR_FILE.exists()
current_build = redbot.core.VersionInfo(*await cog.config.current_build()) current_build = redbot.core.VersionInfo(*await cog.config.current_build())
session = ClientSession(loop=loop)
if not jar_exists or current_build < redbot.core.version_info: if not jar_exists or current_build < redbot.core.version_info:
log.info("Downloading Lavalink.jar")
LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True) LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
async with ClientSession(loop=loop) as session:
await download_lavalink(session) await download_lavalink(session)
await cog.config.current_build.set(redbot.core.version_info.to_json()) await cog.config.current_build.set(redbot.core.version_info.to_json())
session.close()
shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE)) shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE))
@@ -52,4 +53,5 @@ async def setup(bot: commands.Bot):
await start_lavalink_server(bot.loop) await start_lavalink_server(bot.loop)
bot.add_cog(cog) bot.add_cog(cog)
bot.loop.create_task(cog.disconnect_timer())
bot.loop.create_task(cog.init_config()) bot.loop.create_task(cog.init_config())

View File

@@ -6,6 +6,7 @@ import heapq
import lavalink import lavalink
import math import math
import re import re
import time
import redbot.core import redbot.core
from redbot.core import Config, commands, checks, bank from redbot.core import Config, commands, checks, bank
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS, prev_page, next_page, close_menu from redbot.core.utils.menus import menu, DEFAULT_CONTROLS, prev_page, next_page, close_menu
@@ -14,13 +15,12 @@ from .manager import shutdown_lavalink_server
_ = Translator("Audio", __file__) _ = Translator("Audio", __file__)
__version__ = "0.0.6a" __version__ = "0.0.6d"
__author__ = ["aikaterna", "billy/bollo/ati"] __author__ = ["aikaterna", "billy/bollo/ati"]
@cog_i18n(_) @cog_i18n(_)
class Audio: class Audio:
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.config = Config.get_conf(self, 2711759130, force_registration=True) self.config = Config.get_conf(self, 2711759130, force_registration=True)
@@ -38,12 +38,15 @@ class Audio:
default_guild = { default_guild = {
"dj_enabled": False, "dj_enabled": False,
"dj_role": None, "dj_role": None,
"emptydc_enabled": False,
"emptydc_timer": 0,
"jukebox": False, "jukebox": False,
"jukebox_price": 0, "jukebox_price": 0,
"playlists": {}, "playlists": {},
"notify": False, "notify": False,
"repeat": False, "repeat": False,
"shuffle": False, "shuffle": False,
"thumbnail": False,
"volume": 100, "volume": 100,
"vote_enabled": False, "vote_enabled": False,
"vote_percent": 0, "vote_percent": 0,
@@ -70,6 +73,13 @@ class Audio:
) )
lavalink.register_event_listener(self.event_handler) lavalink.register_event_listener(self.event_handler)
async def _get_embed_colour(self, channel: discord.abc.GuildChannel):
# Unfortunately we need this for when context is unavailable.
if await self.bot.db.guild(channel.guild).use_bot_color():
return channel.guild.me.color
else:
return self.bot.color
async def event_handler(self, player, event_type, extra): async def event_handler(self, player, event_type, extra):
notify = await self.config.guild(player.channel.guild).notify() notify = await self.config.guild(player.channel.guild).notify()
status = await self.config.status() status = await self.config.status()
@@ -99,10 +109,15 @@ class Audio:
except discord.errors.NotFound: except discord.errors.NotFound:
pass pass
embed = discord.Embed( embed = discord.Embed(
colour=notify_channel.guild.me.top_role.colour, colour=(await self._get_embed_colour(notify_channel)),
title="Now Playing", title="Now Playing",
description="**[{}]({})**".format(player.current.title, player.current.uri), description="**[{}]({})**".format(player.current.title, player.current.uri),
) )
if (
await self.config.guild(player.channel.guild).thumbnail()
and player.current.thumbnail
):
embed.set_thumbnail(url=player.current.thumbnail)
notify_message = await notify_channel.send(embed=embed) notify_message = await notify_channel.send(embed=embed)
player.store("notify_message", notify_message) player.store("notify_message", notify_message)
@@ -128,7 +143,7 @@ class Audio:
if notify_channel: if notify_channel:
notify_channel = self.bot.get_channel(notify_channel) notify_channel = self.bot.get_channel(notify_channel)
embed = discord.Embed( embed = discord.Embed(
colour=notify_channel.guild.me.top_role.colour, title="Queue ended." colour=(await self._get_embed_colour(notify_channel)), title="Queue ended."
) )
await notify_channel.send(embed=embed) await notify_channel.send(embed=embed)
@@ -154,7 +169,7 @@ class Audio:
if message_channel: if message_channel:
message_channel = self.bot.get_channel(message_channel) message_channel = self.bot.get_channel(message_channel)
embed = discord.Embed( embed = discord.Embed(
colour=message_channel.guild.me.top_role.colour, colour=(await self._get_embed_colour(message_channel)),
title="Track Error", title="Track Error",
description="{}\n**[{}]({})**".format( description="{}\n**[{}]({})**".format(
extra, player.current.title, player.current.uri extra, player.current.title, player.current.uri
@@ -168,8 +183,7 @@ class Audio:
@commands.guild_only() @commands.guild_only()
async def audioset(self, ctx): async def audioset(self, ctx):
"""Music configuration options.""" """Music configuration options."""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@audioset.command() @audioset.command()
@checks.admin_or_permissions(manage_roles=True) @checks.admin_or_permissions(manage_roles=True)
@@ -197,6 +211,26 @@ class Audio:
await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled) await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled)
await self._embed_msg(ctx, "DJ role enabled: {}.".format(not dj_enabled)) await self._embed_msg(ctx, "DJ role enabled: {}.".format(not dj_enabled))
@audioset.command()
@checks.mod_or_permissions(administrator=True)
async def emptydisconnect(self, ctx, seconds: int):
"""Auto-disconnection after x seconds while stopped. 0 to disable."""
if seconds < 0:
return await self._embed_msg(ctx, "Can't be less than zero.")
if seconds < 10 and seconds > 0:
seconds = 10
if seconds == 0:
enabled = False
await self._embed_msg(ctx, "Empty disconnect disabled.")
else:
enabled = True
await self._embed_msg(
ctx, "Empty disconnect timer set to {}.".format(self._dynamic_time(seconds))
)
await self.config.guild(ctx.guild).emptydc_timer.set(seconds)
await self.config.guild(ctx.guild).emptydc_enabled.set(enabled)
@audioset.command() @audioset.command()
@checks.admin_or_permissions(manage_roles=True) @checks.admin_or_permissions(manage_roles=True)
async def role(self, ctx, role_name: discord.Role): async def role(self, ctx, role_name: discord.Role):
@@ -242,12 +276,17 @@ class Audio:
global_data = await self.config.all() global_data = await self.config.all()
dj_role_obj = discord.utils.get(ctx.guild.roles, id=data["dj_role"]) dj_role_obj = discord.utils.get(ctx.guild.roles, id=data["dj_role"])
dj_enabled = data["dj_enabled"] dj_enabled = data["dj_enabled"]
emptydc_enabled = data["emptydc_enabled"]
emptydc_timer = data["emptydc_timer"]
jukebox = data["jukebox"] jukebox = data["jukebox"]
jukebox_price = data["jukebox_price"] jukebox_price = data["jukebox_price"]
thumbnail = data["thumbnail"]
jarbuild = redbot.core.__version__ jarbuild = redbot.core.__version__
vote_percent = data["vote_percent"] vote_percent = data["vote_percent"]
msg = "```ini\n" "----Server Settings----\n" msg = "```ini\n" "----Server Settings----\n"
if emptydc_enabled:
msg += "Disconnect timer: [{0}]\n".format(self._dynamic_time(emptydc_timer))
if dj_enabled: if dj_enabled:
msg += "DJ Role: [{}]\n".format(dj_role_obj.name) msg += "DJ Role: [{}]\n".format(dj_role_obj.name)
if jukebox: if jukebox:
@@ -259,6 +298,8 @@ class Audio:
"Song notify msgs: [{notify}]\n" "Song notify msgs: [{notify}]\n"
"Songs as status: [{status}]\n".format(**global_data, **data) "Songs as status: [{status}]\n".format(**global_data, **data)
) )
if thumbnail:
msg += "Thumbnails: [{0}]\n".format(thumbnail)
if vote_percent > 0: if vote_percent > 0:
msg += ( msg += (
"Vote skip: [{vote_enabled}]\n" "Skip percentage: [{vote_percent}%]\n" "Vote skip: [{vote_enabled}]\n" "Skip percentage: [{vote_percent}%]\n"
@@ -270,9 +311,17 @@ class Audio:
"External server: [{use_external_lavalink}]```" "External server: [{use_external_lavalink}]```"
).format(__version__, jarbuild, **global_data) ).format(__version__, jarbuild, **global_data)
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, description=msg) embed = discord.Embed(colour=(await ctx.embed_colour()), description=msg)
return await ctx.send(embed=embed) return await ctx.send(embed=embed)
@audioset.command()
@checks.mod_or_permissions(administrator=True)
async def thumbnail(self, ctx):
"""Toggle displaying a thumbnail on audio messages."""
thumbnail = await self.config.guild(ctx.guild).thumbnail()
await self.config.guild(ctx.guild).thumbnail.set(not thumbnail)
await self._embed_msg(ctx, "Thumbnail display: {}.".format(not thumbnail))
@audioset.command() @audioset.command()
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def vote(self, ctx, percent: int): async def vote(self, ctx, percent: int):
@@ -330,7 +379,7 @@ class Audio:
else: else:
servers = "\n".join(server_list) servers = "\n".join(server_list)
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Connected in {} servers:".format(server_num), title="Connected in {} servers:".format(server_num),
description=servers, description=servers,
) )
@@ -408,8 +457,10 @@ class Audio:
pass pass
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, title="Now Playing", description=song colour=(await ctx.embed_colour()), title="Now Playing", description=song
) )
if await self.config.guild(ctx.guild).thumbnail() and player.current.thumbnail:
embed.set_thumbnail(url=player.current.thumbnail)
message = await ctx.send(embed=embed) message = await ctx.send(embed=embed)
player.store("np_message", message) player.store("np_message", message)
@@ -426,7 +477,11 @@ class Audio:
await message.add_reaction(expected[i]) await message.add_reaction(expected[i])
def check(r, u): def check(r, u):
return r.message.id == message.id and u == ctx.message.author return (
r.message.id == message.id
and u == ctx.message.author
and any(e in str(r.emoji) for e in expected)
)
try: try:
(r, u) = await self.bot.wait_for("reaction_add", check=check, timeout=10.0) (r, u) = await self.bot.wait_for("reaction_add", check=check, timeout=10.0)
@@ -471,7 +526,7 @@ class Audio:
if player.current and not player.paused and command != "resume": if player.current and not player.paused and command != "resume":
await player.pause() await player.pause()
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Track Paused", title="Track Paused",
description="**[{}]({})**".format(player.current.title, player.current.uri), description="**[{}]({})**".format(player.current.title, player.current.uri),
) )
@@ -480,7 +535,7 @@ class Audio:
if player.paused and command != "pause": if player.paused and command != "pause":
await player.pause(False) await player.pause(False)
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Track Resumed", title="Track Resumed",
description="**[{}]({})**".format(player.current.title, player.current.uri), description="**[{}]({})**".format(player.current.title, player.current.uri),
) )
@@ -542,7 +597,7 @@ class Audio:
queue_user = ["{}: {:g}%".format(x[0], x[1]) for x in top_queue_users] queue_user = ["{}: {:g}%".format(x[0], x[1]) for x in top_queue_users]
queue_user_list = "\n".join(queue_user) queue_user_list = "\n".join(queue_user)
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Queued and playing songs:", title="Queued and playing songs:",
description=queue_user_list, description=queue_user_list,
) )
@@ -557,6 +612,12 @@ class Audio:
shuffle = await self.config.guild(ctx.guild).shuffle() shuffle = await self.config.guild(ctx.guild).shuffle()
if not self._player_check(ctx): if not self._player_check(ctx):
try: try:
if not ctx.author.voice.channel.permissions_for(
ctx.me
).connect == True or self._userlimit(ctx.author.voice.channel):
return await self._embed_msg(
ctx, "I don't have permission to connect to your channel."
)
await lavalink.connect(ctx.author.voice.channel) await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow()) player.store("connect", datetime.datetime.utcnow())
@@ -590,20 +651,20 @@ class Audio:
queue_duration = await self._queue_duration(ctx) queue_duration = await self._queue_duration(ctx)
queue_total_duration = lavalink.utils.format_time(queue_duration) queue_total_duration = lavalink.utils.format_time(queue_duration)
before_queue_length = len(player.queue) + 1 before_queue_length = len(player.queue)
if "list" in query and "ytsearch:" not in query: if "list" in query and "ytsearch:" not in query:
for track in tracks: for track in tracks:
player.add(ctx.author, track) player.add(ctx.author, track)
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Playlist Enqueued", title="Playlist Enqueued",
description="Added {} tracks to the queue.".format(len(tracks)), description="Added {} tracks to the queue.".format(len(tracks)),
) )
if not shuffle and queue_duration > 0: if not shuffle and queue_duration > 0:
embed.set_footer( embed.set_footer(
text="{} until start of playlist playback: starts at #{} in queue".format( text="{} until start of playlist playback: starts at #{} in queue".format(
queue_total_duration, before_queue_length queue_total_duration, before_queue_length + 1
) )
) )
if not player.current: if not player.current:
@@ -612,18 +673,18 @@ class Audio:
single_track = tracks[0] single_track = tracks[0]
player.add(ctx.author, single_track) player.add(ctx.author, single_track)
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Track Enqueued", title="Track Enqueued",
description="**[{}]({})**".format(single_track.title, single_track.uri), description="**[{}]({})**".format(single_track.title, single_track.uri),
) )
if not shuffle and queue_duration > 0: if not shuffle and queue_duration > 0:
embed.set_footer( embed.set_footer(
text="{} until track playback: #{} in queue".format( text="{} until track playback: #{} in queue".format(
queue_total_duration, before_queue_length queue_total_duration, before_queue_length + 1
) )
) )
elif queue_duration > 0: elif queue_duration > 0:
embed.set_footer(text="#{} in queue".format(len(player.queue) + 1)) embed.set_footer(text="#{} in queue".format(len(player.queue)))
if not player.current: if not player.current:
await player.play() await player.play()
await ctx.send(embed=embed) await ctx.send(embed=embed)
@@ -632,8 +693,7 @@ class Audio:
@commands.guild_only() @commands.guild_only()
async def playlist(self, ctx): async def playlist(self, ctx):
"""Playlist configuration options.""" """Playlist configuration options."""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@playlist.command(name="append") @playlist.command(name="append")
async def _playlist_append(self, ctx, playlist_name, *url): async def _playlist_append(self, ctx, playlist_name, *url):
@@ -680,7 +740,9 @@ class Audio:
return await self._embed_msg( return await self._embed_msg(
ctx, "Playlist name already exists, try again with a different name." ctx, "Playlist name already exists, try again with a different name."
) )
playlist_name = playlist_name.split(" ")[0].strip('"')
playlist_list = self._to_json(ctx, None, None) playlist_list = self._to_json(ctx, None, None)
async with self.config.guild(ctx.guild).playlists() as playlists:
playlists[playlist_name] = playlist_list playlists[playlist_name] = playlist_list
await self._embed_msg(ctx, "Empty playlist {} created.".format(playlist_name)) await self._embed_msg(ctx, "Empty playlist {} created.".format(playlist_name))
@@ -717,7 +779,7 @@ class Audio:
else: else:
playlist_url = "URL: <{}>".format(playlist_url) playlist_url = "URL: <{}>".format(playlist_url)
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Playlist info for {}:".format(playlist_name), title="Playlist info for {}:".format(playlist_name),
description="Author: **{}**\n{}".format(author_obj, playlist_url), description="Author: **{}**\n{}".format(author_obj, playlist_url),
) )
@@ -734,12 +796,13 @@ class Audio:
abc_names = sorted(playlist_list, key=str.lower) abc_names = sorted(playlist_list, key=str.lower)
all_playlists = ", ".join(abc_names) all_playlists = ", ".join(abc_names)
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Playlists for {}:".format(ctx.guild.name), title="Playlists for {}:".format(ctx.guild.name),
description=all_playlists, description=all_playlists,
) )
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.cooldown(1, 15, discord.ext.commands.BucketType.guild)
@playlist.command(name="queue") @playlist.command(name="queue")
async def _playlist_queue(self, ctx, playlist_name=None): async def _playlist_queue(self, ctx, playlist_name=None):
"""Save the queue to a playlist.""" """Save the queue to a playlist."""
@@ -766,11 +829,11 @@ class Audio:
await self._embed_msg(ctx, "Please enter a name for this playlist.") await self._embed_msg(ctx, "Please enter a name for this playlist.")
def check(m): def check(m):
return m.author == ctx.author return m.author == ctx.author and not m.content.startswith(ctx.prefix)
try: try:
playlist_name_msg = await ctx.bot.wait_for("message", timeout=15.0, check=check) playlist_name_msg = await ctx.bot.wait_for("message", timeout=15.0, check=check)
playlist_name = str(playlist_name_msg.content) playlist_name = playlist_name_msg.content.split(" ")[0].strip('"')
if len(playlist_name) > 20: if len(playlist_name) > 20:
return await self._embed_msg(ctx, "Try the command again with a shorter name.") return await self._embed_msg(ctx, "Try the command again with a shorter name.")
if playlist_name in playlists: if playlist_name in playlists:
@@ -781,11 +844,12 @@ class Audio:
return await self._embed_msg(ctx, "No playlist name entered, try again later.") return await self._embed_msg(ctx, "No playlist name entered, try again later.")
playlist_list = self._to_json(ctx, None, tracklist) playlist_list = self._to_json(ctx, None, tracklist)
async with self.config.guild(ctx.guild).playlists() as playlists: async with self.config.guild(ctx.guild).playlists() as playlists:
playlist_name = playlist_name.split(" ")[0].strip('"')
playlists[playlist_name] = playlist_list playlists[playlist_name] = playlist_list
await self._embed_msg( await self._embed_msg(
ctx, ctx,
"Playlist {} saved from current queue: {} tracks added.".format( "Playlist {} saved from current queue: {} tracks added.".format(
playlist_name, len(tracklist) playlist_name.split(" ")[0].strip('"'), len(tracklist)
), ),
) )
@@ -833,6 +897,7 @@ class Audio:
playlist_list = self._to_json(ctx, playlist_url, tracklist) playlist_list = self._to_json(ctx, playlist_url, tracklist)
if tracklist is not None: if tracklist is not None:
async with self.config.guild(ctx.guild).playlists() as playlists: async with self.config.guild(ctx.guild).playlists() as playlists:
playlist_name = playlist_name.split(" ")[0].strip('"')
playlists[playlist_name] = playlist_list playlists[playlist_name] = playlist_list
return await self._embed_msg( return await self._embed_msg(
ctx, ctx,
@@ -853,7 +918,7 @@ class Audio:
player.add(author_obj, lavalink.rest_api.Track(data=track)) player.add(author_obj, lavalink.rest_api.Track(data=track))
track_count = track_count + 1 track_count = track_count + 1
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Playlist Enqueued", title="Playlist Enqueued",
description="Added {} tracks to the queue.".format(track_count), description="Added {} tracks to the queue.".format(track_count),
) )
@@ -891,8 +956,11 @@ class Audio:
file_suffix = file_url.rsplit(".", 1)[1] file_suffix = file_url.rsplit(".", 1)[1]
if file_suffix != "txt": if file_suffix != "txt":
return await self._embed_msg(ctx, "Only playlist files can be uploaded.") return await self._embed_msg(ctx, "Only playlist files can be uploaded.")
try:
async with self.session.request("GET", file_url) as r: async with self.session.request("GET", file_url) as r:
v2_playlist = await r.json(content_type="text/plain") v2_playlist = await r.json(content_type="text/plain")
except UnicodeDecodeError:
return await self._embed_msg(ctx, "Not a valid playlist file.")
try: try:
v2_playlist_url = v2_playlist["link"] v2_playlist_url = v2_playlist["link"]
except KeyError: except KeyError:
@@ -913,7 +981,7 @@ class Audio:
except KeyError: except KeyError:
pass pass
embed1 = discord.Embed( embed1 = discord.Embed(
colour=ctx.guild.me.top_role.colour, title="Please wait, adding tracks..." colour=(await ctx.embed_colour()), title="Please wait, adding tracks..."
) )
playlist_msg = await ctx.send(embed=embed1) playlist_msg = await ctx.send(embed=embed1)
for song_url in v2_playlist["playlist"]: for song_url in v2_playlist["playlist"]:
@@ -926,7 +994,7 @@ class Audio:
pass pass
if track_count % 5 == 0: if track_count % 5 == 0:
embed2 = discord.Embed( embed2 = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Loading track {}/{}...".format( title="Loading track {}/{}...".format(
track_count, len(v2_playlist["playlist"]) track_count, len(v2_playlist["playlist"])
), ),
@@ -946,7 +1014,7 @@ class Audio:
else: else:
msg = "Added {} tracks from the {} playlist.".format(track_count, v2_playlist_name) msg = "Added {} tracks from the {} playlist.".format(track_count, v2_playlist_name)
embed3 = discord.Embed( embed3 = discord.Embed(
colour=ctx.guild.me.top_role.colour, title="Playlist Saved", description=msg colour=(await ctx.embed_colour()), title="Playlist Saved", description=msg
) )
await playlist_msg.edit(embed=embed3) await playlist_msg.edit(embed=embed3)
else: else:
@@ -961,6 +1029,12 @@ class Audio:
return False return False
if not self._player_check(ctx): if not self._player_check(ctx):
try: try:
if not ctx.author.voice.channel.permissions_for(
ctx.me
).connect == True or self._userlimit(ctx.author.voice.channel):
return await self._embed_msg(
ctx, "I don't have permission to connect to your channel."
)
await lavalink.connect(ctx.author.voice.channel) await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow()) player.store("connect", datetime.datetime.utcnow())
@@ -1036,7 +1110,7 @@ class Audio:
player.queue.pop(queue_len) player.queue.pop(queue_len)
await player.skip() await player.skip()
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Replaying Track", title="Replaying Track",
description="**[{}]({})**".format(player.current.title, player.current.uri), description="**[{}]({})**".format(player.current.title, player.current.uri),
) )
@@ -1102,10 +1176,12 @@ class Audio:
) )
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Queue for " + ctx.guild.name, title="Queue for " + ctx.guild.name,
description=queue_list, description=queue_list,
) )
if await self.config.guild(ctx.guild).thumbnail() and player.current.thumbnail:
embed.set_thumbnail(url=player.current.thumbnail)
queue_duration = await self._queue_duration(ctx) queue_duration = await self._queue_duration(ctx)
queue_total_duration = lavalink.utils.format_time(queue_duration) queue_total_duration = lavalink.utils.format_time(queue_duration)
text = "Page {}/{} | {} tracks, {} remaining".format( text = "Page {}/{} | {} tracks, {} remaining".format(
@@ -1178,6 +1254,12 @@ class Audio:
""" """
if not self._player_check(ctx): if not self._player_check(ctx):
try: try:
if not ctx.author.voice.channel.permissions_for(
ctx.me
).connect == True or self._userlimit(ctx.author.voice.channel):
return await self._embed_msg(
ctx, "I don't have permission to connect to your channel."
)
await lavalink.connect(ctx.author.voice.channel) await lavalink.connect(ctx.author.voice.channel)
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow()) player.store("connect", datetime.datetime.utcnow())
@@ -1195,13 +1277,12 @@ class Audio:
query = query.strip("<>") query = query.strip("<>")
if query.startswith("list "): if query.startswith("list "):
query = "ytsearch:{}".format(query.lstrip("list ")) query = "ytsearch:{}".format(query.replace("list ", ""))
tracks = await player.get_tracks(query) tracks = await player.get_tracks(query)
if not tracks: if not tracks:
return await self._embed_msg(ctx, "Nothing found 👀") return await self._embed_msg(ctx, "Nothing found.")
songembed = discord.Embed( songembed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()), title="Queued {} track(s).".format(len(tracks))
title="Queued {} track(s).".format(len(tracks)),
) )
queue_duration = await self._queue_duration(ctx) queue_duration = await self._queue_duration(ctx)
queue_total_duration = lavalink.utils.format_time(queue_duration) queue_total_duration = lavalink.utils.format_time(queue_duration)
@@ -1217,12 +1298,12 @@ class Audio:
await player.play() await player.play()
return await ctx.send(embed=songembed) return await ctx.send(embed=songembed)
if query.startswith("sc "): if query.startswith("sc "):
query = "scsearch:{}".format(query.lstrip("sc ")) query = "scsearch:{}".format(query.replace("sc ", ""))
elif not query.startswith("http"): elif not query.startswith("http"):
query = "ytsearch:{}".format(query) query = "ytsearch:{}".format(query)
tracks = await player.get_tracks(query) tracks = await player.get_tracks(query)
if not tracks: if not tracks:
return await self._embed_msg(ctx, "Nothing found 👀") return await self._embed_msg(ctx, "Nothing found.")
len_search_pages = math.ceil(len(tracks) / 5) len_search_pages = math.ceil(len(tracks) / 5)
search_page_list = [] search_page_list = []
@@ -1281,7 +1362,7 @@ class Audio:
search_choice = tracks[-1] search_choice = tracks[-1]
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Track Enqueued", title="Track Enqueued",
description="**[{}]({})**".format(search_choice.title, search_choice.uri), description="**[{}]({})**".format(search_choice.title, search_choice.uri),
) )
@@ -1318,7 +1399,7 @@ class Audio:
search_track_num, track.title, track.uri search_track_num, track.title, track.uri
) )
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, title="Tracks Found:", description=search_list colour=(await ctx.embed_colour()), title="Tracks Found:", description=search_list
) )
embed.set_footer( embed.set_footer(
text="Page {}/{} | {} search results".format(page_num, search_num_pages, len(tracks)) text="Page {}/{} | {} search results".format(page_num, search_num_pages, len(tracks))
@@ -1485,26 +1566,28 @@ class Audio:
else: else:
return False return False
@staticmethod async def _skip_action(self, ctx):
async def _skip_action(ctx):
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
if not player.queue: if not player.queue:
try:
pos, dur = player.position, player.current.length pos, dur = player.position, player.current.length
except AttributeError:
return await self._embed_msg(ctx, "There's nothing in the queue.")
time_remain = lavalink.utils.format_time(dur - pos) time_remain = lavalink.utils.format_time(dur - pos)
if player.current.is_stream: if player.current.is_stream:
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, title="There's nothing in the queue." colour=(await ctx.embed_colour()), title="There's nothing in the queue."
) )
embed.set_footer(text="Currently livestreaming {}".format(player.current.title)) embed.set_footer(text="Currently livestreaming {}".format(player.current.title))
else: else:
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, title="There's nothing in the queue." colour=(await ctx.embed_colour()), title="There's nothing in the queue."
) )
embed.set_footer(text="{} left on {}".format(time_remain, player.current.title)) embed.set_footer(text="{} left on {}".format(time_remain, player.current.title))
return await ctx.send(embed=embed) return await ctx.send(embed=embed)
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Track Skipped", title="Track Skipped",
description="**[{}]({})**".format(player.current.title, player.current.uri), description="**[{}]({})**".format(player.current.title, player.current.uri),
) )
@@ -1552,7 +1635,7 @@ class Audio:
if not vol: if not vol:
vol = await self.config.guild(ctx.guild).volume() vol = await self.config.guild(ctx.guild).volume()
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Current Volume:", title="Current Volume:",
description=str(vol) + "%", description=str(vol) + "%",
) )
@@ -1582,7 +1665,7 @@ class Audio:
if self._player_check(ctx): if self._player_check(ctx):
await lavalink.get_player(ctx.guild.id).set_volume(vol) await lavalink.get_player(ctx.guild.id).set_volume(vol)
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, title="Volume:", description=str(vol) + "%" colour=(await ctx.embed_colour()), title="Volume:", description=str(vol) + "%"
) )
if not self._player_check(ctx): if not self._player_check(ctx):
embed.set_footer(text="Nothing playing.") embed.set_footer(text="Nothing playing.")
@@ -1593,8 +1676,7 @@ class Audio:
@checks.is_owner() @checks.is_owner()
async def llsetup(self, ctx): async def llsetup(self, ctx):
"""Lavalink server configuration options.""" """Lavalink server configuration options."""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@llsetup.command() @llsetup.command()
async def external(self, ctx): async def external(self, ctx):
@@ -1607,7 +1689,7 @@ class Audio:
await self.config.rest_port.set(2333) await self.config.rest_port.set(2333)
await self.config.ws_port.set(2332) await self.config.ws_port.set(2332)
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="External lavalink server: {}.".format(not external), title="External lavalink server: {}.".format(not external),
) )
embed.set_footer(text="Defaults reset.") embed.set_footer(text="Defaults reset.")
@@ -1621,7 +1703,7 @@ class Audio:
await self.config.host.set(host) await self.config.host.set(host)
if await self._check_external(): if await self._check_external():
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, title="Host set to {}.".format(host) colour=(await ctx.embed_colour()), title="Host set to {}.".format(host)
) )
embed.set_footer(text="External lavalink server set to True.") embed.set_footer(text="External lavalink server set to True.")
await ctx.send(embed=embed) await ctx.send(embed=embed)
@@ -1634,7 +1716,7 @@ class Audio:
await self.config.password.set(str(password)) await self.config.password.set(str(password))
if await self._check_external(): if await self._check_external():
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Server password set to {}.".format(password), title="Server password set to {}.".format(password),
) )
embed.set_footer(text="External lavalink server set to True.") embed.set_footer(text="External lavalink server set to True.")
@@ -1648,7 +1730,7 @@ class Audio:
await self.config.rest_port.set(rest_port) await self.config.rest_port.set(rest_port)
if await self._check_external(): if await self._check_external():
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, title="REST port set to {}.".format(rest_port) colour=(await ctx.embed_colour()), title="REST port set to {}.".format(rest_port)
) )
embed.set_footer(text="External lavalink server set to True.") embed.set_footer(text="External lavalink server set to True.")
await ctx.send(embed=embed) await ctx.send(embed=embed)
@@ -1661,7 +1743,7 @@ class Audio:
await self.config.ws_port.set(ws_port) await self.config.ws_port.set(ws_port)
if await self._check_external(): if await self._check_external():
embed = discord.Embed( embed = discord.Embed(
colour=ctx.guild.me.top_role.colour, colour=(await ctx.embed_colour()),
title="Websocket port set to {}.".format(ws_port), title="Websocket port set to {}.".format(ws_port),
) )
embed.set_footer(text="External lavalink server set to True.") embed.set_footer(text="External lavalink server set to True.")
@@ -1711,6 +1793,34 @@ class Audio:
if player.volume != volume: if player.volume != volume:
await player.set_volume(volume) await player.set_volume(volume)
async def disconnect_timer(self):
stop_times = {}
while self == self.bot.get_cog("Audio"):
for p in lavalink.players:
server = p.channel.guild
if server.id not in stop_times:
stop_times[server.id] = None
if [self.bot.user] == p.channel.members:
if stop_times[server.id] is None:
stop_times[server.id] = int(time.time())
for sid in stop_times:
server_obj = self.bot.get_guild(sid)
emptydc_enabled = await self.config.guild(server_obj).emptydc_enabled()
if emptydc_enabled:
if stop_times[sid] is not None and [self.bot.user] == p.channel.members:
emptydc_timer = await self.config.guild(server_obj).emptydc_timer()
if stop_times[sid] and (
int(time.time()) - stop_times[sid] > emptydc_timer
):
stop_times[sid] = None
await lavalink.get_player(sid).disconnect()
await asyncio.sleep(5)
@staticmethod @staticmethod
async def _draw_time(ctx): async def _draw_time(ctx):
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
@@ -1752,7 +1862,7 @@ class Audio:
@staticmethod @staticmethod
async def _embed_msg(ctx, title): async def _embed_msg(ctx, title):
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title=title) embed = discord.Embed(colour=(await ctx.embed_colour()), title=title)
await ctx.send(embed=embed) await ctx.send(embed=embed)
async def _get_playing(self, ctx): async def _get_playing(self, ctx):
@@ -1826,6 +1936,15 @@ class Audio:
track_obj[key] = value track_obj[key] = value
return track_obj return track_obj
@staticmethod
def _userlimit(channel):
if channel.user_limit == 0:
return False
if channel.user_limit < len(channel.members) + 1:
return True
else:
return False
async def on_voice_state_update(self, member, before, after): async def on_voice_state_update(self, member, before, after):
if after.channel != before.channel: if after.channel != before.channel:
try: try:
@@ -1834,7 +1953,7 @@ class Audio:
pass pass
def __unload(self): def __unload(self):
self.session.close() self.session.detach()
lavalink.unregister_event_listener(self.event_handler) lavalink.unregister_event_listener(self.event_handler)
self.bot.loop.create_task(lavalink.close()) self.bot.loop.create_task(lavalink.close())
shutdown_lavalink_server() shutdown_lavalink_server()

View File

@@ -1,41 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: ../audio.py:25 ../audio.py:45
msgid "Join a voice channel first!"
msgstr ""
#: ../audio.py:33
msgid "Let's play a file that exists pls"
msgstr ""
#: ../audio.py:38 ../audio.py:58
msgid "{} is playing a song..."
msgstr ""
#: ../audio.py:48
msgid "Youtube links pls"
msgstr ""
#: ../audio.py:67 ../audio.py:77 ../audio.py:87 ../audio.py:97
msgid "I'm not even connected to a voice channel!"
msgstr ""
#: ../audio.py:95
msgid "Volume set."
msgstr ""

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../audio.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

View File

@@ -1,9 +1,14 @@
import shlex import shlex
import shutil import shutil
import asyncio import asyncio
from subprocess import Popen, DEVNULL, PIPE import asyncio.subprocess
import os import os
import logging import logging
import re
from subprocess import Popen, DEVNULL
from typing import Optional, Tuple
_JavaVersion = Tuple[int, int]
log = logging.getLogger("red.audio.manager") log = logging.getLogger("red.audio.manager")
@@ -36,29 +41,48 @@ async def monitor_lavalink_server(loop):
) )
async def has_java(loop): async def has_java(loop) -> Tuple[bool, Optional[_JavaVersion]]:
java_available = shutil.which("java") is not None java_available = shutil.which("java") is not None
if not java_available: if not java_available:
return False return False, None
version = await get_java_version(loop) version = await get_java_version(loop)
return version >= (1, 8), version return (2, 0) > version >= (1, 8) or version >= (8, 0), version
async def get_java_version(loop): async def get_java_version(loop) -> _JavaVersion:
""" """
This assumes we've already checked that java exists. This assumes we've already checked that java exists.
""" """
proc = Popen(shlex.split("java -version", posix=os.name == "posix"), stdout=PIPE, stderr=PIPE) _proc: asyncio.subprocess.Process = await asyncio.create_subprocess_exec(
_, err = proc.communicate() "java",
"-version",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
loop=loop,
)
# java -version outputs to stderr
_, err = await _proc.communicate()
version_info = str(err, encoding="utf-8") version_info: str = err.decode("utf-8")
# We expect the output to look something like:
# $ java -version
# ...
# ... version "MAJOR.MINOR.PATCH[_BUILD]" ...
# ...
# We only care about the major and minor parts though.
version_line_re = re.compile(r'version "(?P<major>\d+).(?P<minor>\d+).\d+(?:_\d+)?"')
version_line = version_info.split("\n")[0] lines = version_info.splitlines()
version_start = version_line.find('"') for line in lines:
version_string = version_line[version_start + 1 : -1] match = version_line_re.search(line)
major, minor = version_string.split(".")[:2] if match:
return int(major), int(minor) return int(match["major"]), int(match["minor"])
raise RuntimeError(
"The output of `java -version` was unexpected. Please report this issue on Red's "
"issue tracker."
)
async def start_lavalink_server(loop): async def start_lavalink_server(loop):

View File

@@ -17,13 +17,15 @@ def check_global_setting_guildowner():
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
author = ctx.author author = ctx.author
if await ctx.bot.is_owner(author):
return True
if not await bank.is_global(): if not await bank.is_global():
if not isinstance(ctx.channel, discord.abc.GuildChannel): if not isinstance(ctx.channel, discord.abc.GuildChannel):
return False return False
if await ctx.bot.is_owner(author):
return True
permissions = ctx.channel.permissions_for(author) permissions = ctx.channel.permissions_for(author)
return author == ctx.guild.owner or permissions.administrator return author == ctx.guild.owner or permissions.administrator
else:
return await ctx.bot.is_owner(author)
return commands.check(pred) return commands.check(pred)
@@ -36,15 +38,17 @@ def check_global_setting_admin():
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
author = ctx.author author = ctx.author
if await ctx.bot.is_owner(author):
return True
if not await bank.is_global(): if not await bank.is_global():
if not isinstance(ctx.channel, discord.abc.GuildChannel): if not isinstance(ctx.channel, discord.abc.GuildChannel):
return False return False
if await ctx.bot.is_owner(author):
return True
permissions = ctx.channel.permissions_for(author) permissions = ctx.channel.permissions_for(author)
is_guild_owner = author == ctx.guild.owner is_guild_owner = author == ctx.guild.owner
admin_role = await ctx.bot.db.guild(ctx.guild).admin_role() admin_role = await ctx.bot.db.guild(ctx.guild).admin_role()
return admin_role in author.roles or is_guild_owner or permissions.manage_guild return admin_role in author.roles or is_guild_owner or permissions.manage_guild
else:
return await ctx.bot.is_owner(author)
return commands.check(pred) return commands.check(pred)
@@ -58,8 +62,9 @@ class Bank:
# SECTION commands # SECTION commands
@commands.group() @check_global_setting_guildowner()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=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:
@@ -69,17 +74,15 @@ class Bank:
default_balance = await bank._conf.default_balance() default_balance = await bank._conf.default_balance()
else: else:
if not ctx.guild: if not ctx.guild:
await ctx.send_help()
return return
bank_name = await bank._conf.guild(ctx.guild).bank_name() bank_name = await bank._conf.guild(ctx.guild).bank_name()
currency_name = await bank._conf.guild(ctx.guild).currency() currency_name = await bank._conf.guild(ctx.guild).currency()
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\n" "Bank name: {}\n" "Currency: {}\n" "Default balance: {}" "" "Bank settings:\n\nBank name: {}\nCurrency: {}\nDefault balance: {}"
).format(bank_name, currency_name, default_balance) ).format(bank_name, currency_name, default_balance)
await ctx.send(box(settings)) await ctx.send(box(settings))
await ctx.send_help()
@bankset.command(name="toggleglobal") @bankset.command(name="toggleglobal")
@checks.is_owner() @checks.is_owner()

View File

@@ -1,37 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: ../bank.py:68
msgid "global"
msgstr ""
#: ../bank.py:68
msgid "per-guild"
msgstr ""
#: ../bank.py:70
msgid "The bank is now {}."
msgstr ""
#: ../bank.py:77
msgid "Bank's name has been set to {}"
msgstr ""
#: ../bank.py:84
msgid "Currency name has been set to {}"
msgstr ""

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../bank.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

View File

@@ -1,4 +1,6 @@
import re import re
from datetime import datetime, timedelta
from typing import Union, List, Callable
import discord import discord
@@ -49,59 +51,65 @@ class Cleanup:
@staticmethod @staticmethod
async def get_messages_for_deletion( async def get_messages_for_deletion(
ctx: commands.Context, *,
channel: discord.TextChannel, channel: discord.TextChannel,
number, number: int = None,
check=lambda x: True, check: Callable[[discord.Message], bool] = lambda x: True,
limit=100, before: Union[discord.Message, datetime] = None,
before=None, after: Union[discord.Message, datetime] = None,
after=None, delete_pinned: bool = False,
delete_pinned=False, ) -> List[discord.Message]:
) -> list:
""" """
Gets a list of messages meeting the requirements to be deleted. Gets a list of messages meeting the requirements to be deleted.
Generally, the requirements are: Generally, the requirements are:
- We don't have the number of messages to be deleted already - We don't have the number of messages to be deleted already
- The message passes a provided check (if no check is provided, - The message passes a provided check (if no check is provided,
this is automatically true) this is automatically true)
- The message is less than 14 days old - The message is less than 14 days old
- The message is not pinned - The message is not pinned
"""
to_delete = []
too_old = False
while not too_old and len(to_delete) - 1 < number: Warning: Due to the way the API hands messages back in chunks,
message = None passing after and a number together is not advisable.
async for message in channel.history(limit=limit, before=before, after=after): If you need to accomplish this, you should filter messages on
if ( the entire applicable range, rather than use this utility.
(not number or len(to_delete) - 1 < number) """
and check(message)
and (ctx.message.created_at - message.created_at).days < 14 # This isn't actually two weeks ago to allow some wiggle room on API limits
two_weeks_ago = datetime.utcnow() - timedelta(days=14, minutes=-5)
def message_filter(message):
return (
check(message)
and message.created_at > two_weeks_ago
and (delete_pinned or not message.pinned) and (delete_pinned or not message.pinned)
)
if after:
if isinstance(after, discord.Message):
after = after.created_at
after = max(after, two_weeks_ago)
collected = []
async for message in channel.history(
limit=None, before=before, after=after, reverse=False
): ):
to_delete.append(message) if message.created_at < two_weeks_ago:
elif (ctx.message.created_at - message.created_at).days >= 14:
too_old = True
break break
elif number and len(to_delete) >= number: if check(message):
collected.append(message)
if number and number <= len(collected):
break break
if message is None:
break return collected
else:
before = message
return to_delete
@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.""" """Deletes messages."""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@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
): ):
@@ -113,8 +121,11 @@ class Cleanup:
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
is_bot = self.bot.user.bot
if number > 100: if number > 100:
cont = await self.check_100_plus(ctx, number) cont = await self.check_100_plus(ctx, number)
@@ -130,28 +141,22 @@ class Cleanup:
return False return False
to_delete = await self.get_messages_for_deletion( to_delete = await self.get_messages_for_deletion(
ctx, channel=channel,
channel, number=number,
number,
check=check, check=check,
limit=1000,
before=ctx.message, before=ctx.message,
delete_pinned=delete_pinned, delete_pinned=delete_pinned,
) )
reason = "{}({}) deleted {} messages " " containing '{}' in channel {}.".format( reason = "{}({}) deleted {} messages containing '{}' in channel {}.".format(
author.name, author.id, len(to_delete), text, channel.id author.name, author.id, len(to_delete), text, channel.id
) )
log.info(reason) log.info(reason)
if is_bot:
await mass_purge(to_delete, channel) await mass_purge(to_delete, channel)
else:
await slow_deletion(to_delete)
@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
): ):
@@ -160,6 +165,10 @@ class Cleanup:
Examples: Examples:
cleanup user @\u200bTwentysix 2 cleanup user @\u200bTwentysix 2
cleanup user Red 6""" cleanup user Red 6"""
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:
@@ -172,9 +181,7 @@ class Cleanup:
else: else:
_id = member.id _id = member.id
channel = ctx.channel
author = ctx.author author = ctx.author
is_bot = self.bot.user.bot
if number > 100: if number > 100:
cont = await self.check_100_plus(ctx, number) cont = await self.check_100_plus(ctx, number)
@@ -190,11 +197,9 @@ class Cleanup:
return False return False
to_delete = await self.get_messages_for_deletion( to_delete = await self.get_messages_for_deletion(
ctx, channel=channel,
channel, number=number,
number,
check=check, check=check,
limit=1000,
before=ctx.message, before=ctx.message,
delete_pinned=delete_pinned, delete_pinned=delete_pinned,
) )
@@ -205,15 +210,10 @@ class Cleanup:
) )
log.info(reason) log.info(reason)
if is_bot:
# For whatever reason the purge endpoint requires manage_messages
await mass_purge(to_delete, channel) await mass_purge(to_delete, channel)
else:
await slow_deletion(to_delete)
@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. """Deletes all messages after specified message.
@@ -225,24 +225,21 @@ class Cleanup:
""" """
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
is_bot = self.bot.user.bot
if not is_bot:
await ctx.send(_("This command can only be used on bots with " "bot accounts."))
return
try:
after = await channel.get_message(message_id) after = await channel.get_message(message_id)
except discord.NotFound:
if not after: return await ctx.send(_("Message not found."))
await ctx.send(_("Message not found."))
return
to_delete = await self.get_messages_for_deletion( to_delete = await self.get_messages_for_deletion(
ctx, channel, 0, limit=None, after=after, delete_pinned=delete_pinned channel=channel, number=None, after=after, delete_pinned=delete_pinned
) )
reason = "{}({}) deleted {} messages in channel {}." "".format( reason = "{}({}) deleted {} messages in channel {}.".format(
author.name, author.id, len(to_delete), channel.name author.name, author.id, len(to_delete), channel.name
) )
log.info(reason) log.info(reason)
@@ -251,7 +248,6 @@ class Cleanup:
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False): async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
"""Deletes last X messages. """Deletes last X messages.
@@ -259,39 +255,38 @@ class Cleanup:
cleanup messages 26""" 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
is_bot = self.bot.user.bot
if number > 100: if number > 100:
cont = await self.check_100_plus(ctx, number) cont = await self.check_100_plus(ctx, number)
if not cont: if not cont:
return return
to_delete = await self.get_messages_for_deletion( to_delete = await self.get_messages_for_deletion(
ctx, channel, number, limit=1000, before=ctx.message, delete_pinned=delete_pinned channel=channel, number=number, before=ctx.message, delete_pinned=delete_pinned
) )
to_delete.append(ctx.message) to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages in channel {}." "".format( reason = "{}({}) deleted {} messages in channel {}.".format(
author.name, author.id, number, channel.name author.name, author.id, number, channel.name
) )
log.info(reason) log.info(reason)
if is_bot:
await mass_purge(to_delete, channel) await mass_purge(to_delete, channel)
else:
await slow_deletion(to_delete)
@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.""" """Cleans up command messages and messages from the bot."""
channel = ctx.message.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
is_bot = self.bot.user.bot
if number > 100: if number > 100:
cont = await self.check_100_plus(ctx, number) cont = await self.check_100_plus(ctx, number)
@@ -318,11 +313,9 @@ class Cleanup:
return False return False
to_delete = await self.get_messages_for_deletion( to_delete = await self.get_messages_for_deletion(
ctx, channel=channel,
channel, number=number,
number,
check=check, check=check,
limit=1000,
before=ctx.message, before=ctx.message,
delete_pinned=delete_pinned, delete_pinned=delete_pinned,
) )
@@ -335,10 +328,7 @@ class Cleanup:
) )
log.info(reason) log.info(reason)
if is_bot:
await mass_purge(to_delete, channel) await mass_purge(to_delete, channel)
else:
await slow_deletion(to_delete)
@cleanup.command(name="self") @cleanup.command(name="self")
async def cleanup_self( async def cleanup_self(
@@ -360,7 +350,6 @@ class Cleanup:
""" """
channel = ctx.channel channel = ctx.channel
author = ctx.message.author author = ctx.message.author
is_bot = self.bot.user.bot
if number > 100: if number > 100:
cont = await self.check_100_plus(ctx, number) cont = await self.check_100_plus(ctx, number)
@@ -400,20 +389,14 @@ class Cleanup:
return False return False
to_delete = await self.get_messages_for_deletion( to_delete = await self.get_messages_for_deletion(
ctx, channel=channel,
channel, number=number,
number,
check=check, check=check,
limit=1000,
before=ctx.message, before=ctx.message,
delete_pinned=delete_pinned, delete_pinned=delete_pinned,
) )
# Selfbot convenience, delete trigger message if ctx.guild:
if author == self.bot.user:
to_delete.append(ctx.message)
if channel.name:
channel_name = "channel " + channel.name channel_name = "channel " + channel.name
else: else:
channel_name = str(channel) channel_name = str(channel)
@@ -425,7 +408,7 @@ class Cleanup:
) )
log.info(reason) log.info(reason)
if is_bot and can_mass_purge: if can_mass_purge:
await mass_purge(to_delete, channel) await mass_purge(to_delete, channel)
else: else:
await slow_deletion(to_delete) await slow_deletion(to_delete)

View File

@@ -1,25 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: ../cleanup.py:150
msgid "This command can only be used on bots with bot accounts."
msgstr ""
#: ../cleanup.py:157
msgid "Message not found."
msgstr ""

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../cleanup.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

View File

@@ -25,7 +25,6 @@ class AlreadyExists(CCError):
class CommandObj: class CommandObj:
def __init__(self, **kwargs): def __init__(self, **kwargs):
config = kwargs.get("config") config = kwargs.get("config")
self.bot = kwargs.get("bot") self.bot = kwargs.get("bot")
@@ -43,7 +42,7 @@ class CommandObj:
intro = _( intro = _(
"Welcome to the interactive random {} maker!\n" "Welcome to the interactive random {} 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 "
"response to choose from once this {} is " "responses to choose from once this {} is "
"triggered. To exit this interactive menu, type `{}`" "triggered. To exit this interactive menu, type `{}`"
).format("customcommand", "customcommand", "exit()") ).format("customcommand", "customcommand", "exit()")
await ctx.send(intro) await ctx.send(intro)
@@ -75,7 +74,7 @@ class CommandObj:
return ccinfo["response"] return ccinfo["response"]
async def create(self, ctx: commands.Context, command: str, response): async def create(self, ctx: commands.Context, command: str, response):
"""Create a customcommand""" """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):
raise AlreadyExists() raise AlreadyExists()
@@ -132,6 +131,7 @@ class CommandObj:
@cog_i18n(_) @cog_i18n(_)
class CustomCommands: class CustomCommands:
"""Custom commands """Custom commands
Creates commands used to display text""" Creates commands used to display text"""
def __init__(self, bot): def __init__(self, bot):
@@ -141,12 +141,11 @@ class CustomCommands:
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)
@commands.group(aliases=["cc"], no_pm=True) @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"""
if not ctx.invoked_subcommand: pass
await ctx.send_help()
@customcom.group(name="add") @customcom.group(name="add")
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@@ -166,14 +165,14 @@ class CustomCommands:
{server} message.guild {server} message.guild
""" """
if not ctx.invoked_subcommand or isinstance(ctx.invoked_subcommand, commands.Group): pass
await ctx.send_help()
@cc_add.command(name="random") @cc_add.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_add_random(self, ctx: commands.Context, command: str):
""" """
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 is interactive
""" """
channel = ctx.channel channel = ctx.channel
@@ -185,7 +184,7 @@ class CustomCommands:
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 `{}` to edit it.").format(
"{}customcom edit".format(ctx.prefix) "{}customcom edit".format(ctx.prefix)
) )
) )
@@ -196,6 +195,7 @@ class CustomCommands:
@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_add_simple(self, ctx, command: str, *, text):
"""Adds a simple custom command """Adds a simple custom command
Example: Example:
[p]customcom add simple yourcommand Text you want [p]customcom add simple yourcommand Text you want
""" """
@@ -209,7 +209,7 @@ class CustomCommands:
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 `{}` to edit it.").format(
"{}customcom edit".format(ctx.prefix) "{}customcom edit".format(ctx.prefix)
) )
) )
@@ -218,6 +218,7 @@ class CustomCommands:
@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_edit(self, ctx, command: str, *, text=None):
"""Edits a custom command """Edits a custom command
Example: Example:
[p]customcom edit yourcommand Text you want [p]customcom edit yourcommand Text you want
""" """
@@ -229,7 +230,7 @@ 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 `{}` to add it.").format(
"{}customcom add".format(ctx.prefix) "{}customcom add".format(ctx.prefix)
) )
) )

View File

@@ -1,67 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: ../customcom.py:44
msgid ""
"Welcome to the interactive random {} maker!\n"
"Every message you send will be added as one of the random response to choose from once this {} is triggered. To exit this interactive menu, type `{}`"
msgstr ""
#: ../customcom.py:56
msgid "Add a random response:"
msgstr ""
#: ../customcom.py:119
msgid "Do you want to create a 'randomized' cc? {}"
msgstr ""
#: ../customcom.py:126
msgid "What response do you want?"
msgstr ""
#: ../customcom.py:205 ../customcom.py:235
msgid "Custom command successfully added."
msgstr ""
#: ../customcom.py:207 ../customcom.py:237
msgid "This command already exists. Use `{}` to edit it."
msgstr ""
#: ../customcom.py:229
msgid "That command is already a standard command."
msgstr ""
#: ../customcom.py:261
msgid "Custom command successfully edited."
msgstr ""
#: ../customcom.py:263
msgid "That command doesn't exist. Use `{}` to add it."
msgstr ""
#: ../customcom.py:282
msgid "Custom command successfully deleted."
msgstr ""
#: ../customcom.py:284
msgid "That command doesn't exist."
msgstr ""
#: ../customcom.py:294
msgid "There are no custom commands in this guild. Use `{}` to start adding some."
msgstr ""

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../customcom.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

View File

@@ -119,11 +119,10 @@ class SpecResolver(object):
def past_nicknames_conv_spec(self, data: dict): def past_nicknames_conv_spec(self, data: dict):
flatscoped = self.apply_scope(Config.MEMBER, self.flatten_dict(data)) flatscoped = self.apply_scope(Config.MEMBER, self.flatten_dict(data))
ret = {} ret = {}
for k, v in flatscoped.items(): for config_identifiers, v2data in flatscoped.items():
outerkey, innerkey = (*k[:-1],), (k[-1],) if config_identifiers not in ret:
if outerkey not in ret: ret[config_identifiers] = {}
ret[outerkey] = {} ret[config_identifiers].update({("past_nicks",): v2data})
ret[outerkey].update({innerkey: v})
return ret return ret
def customcom_conv_spec(self, data: dict): def customcom_conv_spec(self, data: dict):
@@ -144,18 +143,28 @@ class SpecResolver(object):
ret[outerkey].update({innerkey: ccinfo}) ret[outerkey].update({innerkey: ccinfo})
return ret return ret
async def convert(self, bot: Red, prettyname: str): def get_config_object(self, bot, cogname, attr, _id):
if prettyname not in self.available:
raise NotImplementedError("No Conversion Specs for this")
info = self.available_core_conversions[prettyname]
filepath, converter = info["file"], info["converter"]
(cogname, attr, _id) = info["cfg"]
try: try:
config = getattr(bot.get_cog(cogname), attr) config = getattr(bot.get_cog(cogname), attr)
except (TypeError, AttributeError): except (TypeError, AttributeError):
config = Config.get_conf(None, _id, cog_name=cogname) config = Config.get_conf(None, _id, cog_name=cogname)
return config
def get_conversion_info(self, prettyname: str):
info = self.available_core_conversions[prettyname]
filepath, converter = info["file"], info["converter"]
(cogname, attr, _id) = info["cfg"]
return filepath, converter, cogname, attr, _id
async def convert(self, bot: Red, prettyname: str, config=None):
if prettyname not in self.available:
raise NotImplementedError("No Conversion Specs for this")
filepath, converter, cogname, attr, _id = self.get_conversion_info(prettyname)
if config is None:
config = self.get_config_object(bot, cogname, attr, _id)
try: try:
items = converter(dc.json_load(filepath)) items = converter(dc.json_load(filepath))
await dc(config).dict_import(items) await dc(config).dict_import(items)

View File

@@ -41,7 +41,7 @@ class DataConverter:
) )
) )
while resolver.available: while resolver.available:
menu = _("Please select a set of data to import by number" ", or 'exit' to exit") menu = _("Please select a set of data to import by number, or 'exit' to exit")
for index, entry in enumerate(resolver.available, 1): for index, entry in enumerate(resolver.available, 1):
menu += "\n{}. {}".format(index, entry) menu += "\n{}. {}".format(index, entry)

View File

@@ -1,43 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-03-12 04:35+EDT\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: ../dataconverter.py:38
msgid "There don't seem to be any data files I know how to handle here. Are you sure you gave me the base installation path?"
msgstr ""
#: ../dataconverter.py:43
msgid "Please select a set of data to import by number, or 'exit' to exit"
msgstr ""
#: ../dataconverter.py:59
msgid "Try this again when you are more ready"
msgstr ""
#: ../dataconverter.py:70
msgid "That wasn't a valid choice."
msgstr ""
#: ../dataconverter.py:76
msgid "{} converted."
msgstr ""
#: ../dataconverter.py:80
msgid ""
"There isn't anything else I know how to convert here.\n"
"There might be more things I can convert in the future."
msgstr ""

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../dataconverter.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

View File

@@ -1,9 +1,9 @@
import asyncio import asyncio
import discord import discord
from discord.ext import commands from redbot.core import commands
__all__ = ["install_agreement"] __all__ = ["do_install_agreement"]
REPO_INSTALL_MSG = ( 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"
@@ -16,23 +16,13 @@ REPO_INSTALL_MSG = (
) )
def install_agreement(): async def do_install_agreement(ctx: commands.Context):
downloader = ctx.cog
async def pred(ctx: commands.Context): if downloader is None or downloader.already_agreed:
downloader = ctx.command.instance
if downloader is None:
return True
elif downloader.already_agreed:
return True
elif ctx.invoked_subcommand is None or isinstance(ctx.invoked_subcommand, commands.Group):
return True return True
def does_agree(msg: discord.Message): def does_agree(msg: discord.Message):
return ( return ctx.author == msg.author and ctx.channel == msg.channel and msg.content == "I agree"
ctx.author == msg.author
and ctx.channel == msg.channel
and msg.content == "I agree"
)
await ctx.send(REPO_INSTALL_MSG) await ctx.send(REPO_INSTALL_MSG)
@@ -44,5 +34,3 @@ def install_agreement():
downloader.already_agreed = True downloader.already_agreed = True
return True return True
return commands.check(pred)

View File

@@ -1,11 +1,9 @@
import discord import discord
from discord.ext import commands from redbot.core import commands
from .repo_manager import RepoManager
from .installable import Installable from .installable import Installable
class InstalledCog(commands.Converter): class InstalledCog(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> Installable: async def convert(self, 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:

View File

@@ -15,7 +15,7 @@ from redbot.core.utils.chat_formatting import box, pagify
from redbot.core import commands from redbot.core import commands
from redbot.core.bot import Red from redbot.core.bot import Red
from .checks import install_agreement from .checks import do_install_agreement
from .converters import InstalledCog from .converters import InstalledCog
from .errors import CloningError, ExistingGitRepo from .errors import CloningError, ExistingGitRepo
from .installable import Installable from .installable import Installable
@@ -27,7 +27,6 @@ _ = Translator("Downloader", __file__)
@cog_i18n(_) @cog_i18n(_)
class Downloader: class Downloader:
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.bot = bot self.bot = bot
@@ -211,11 +210,9 @@ class Downloader:
""" """
Command group for managing Downloader repos. Command group for managing Downloader repos.
""" """
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@repo.command(name="add") @repo.command(name="add")
@install_agreement()
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 to Downloader. Add a new repo to Downloader.
@@ -223,6 +220,9 @@ class Downloader:
Name can only contain characters A-z, numbers and underscore Name can only contain characters A-z, numbers and underscore
Branch will default to master if not specified Branch will default to master if not specified
""" """
agreed = await do_install_agreement(ctx)
if not agreed:
return
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)
@@ -234,7 +234,7 @@ class Downloader:
else: else:
await ctx.send(_("Repo `{}` successfully added.").format(name)) await ctx.send(_("Repo `{}` successfully added.").format(name))
if repo.install_msg is not None: if repo.install_msg is not None:
await ctx.send(repo.install_msg) await ctx.send(repo.install_msg.replace("[p]", ctx.prefix))
@repo.command(name="delete") @repo.command(name="delete")
async def _repo_del(self, ctx, repo_name: Repo): async def _repo_del(self, ctx, repo_name: Repo):
@@ -278,8 +278,7 @@ class Downloader:
""" """
Command group for managing installable Cogs. Command group for managing installable Cogs.
""" """
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@cog.command(name="install") @cog.command(name="install")
async def _cog_install(self, ctx, repo_name: Repo, cog_name: str): async def _cog_install(self, ctx, repo_name: Repo, cog_name: str):
@@ -289,24 +288,22 @@ class Downloader:
cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable 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( _("Error, there is no cog by the name of `{}` in the `{}` repo.").format(
cog_name, repo_name.name cog_name, repo_name.name
) )
) )
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 {}, aborting install.".format(
".".join([str(n) for n in cog.min_python_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_name.install_requirements(cog, self.LIB_PATH):
await ctx.send( await ctx.send(
_("Failed to install the required libraries for" " `{}`: `{}`").format( _("Failed to install the required libraries for `{}`: `{}`").format(
cog.name, cog.requirements cog.name, cog.requirements
) )
) )
@@ -320,7 +317,7 @@ class Downloader:
await ctx.send(_("`{}` cog successfully installed.").format(cog_name)) await ctx.send(_("`{}` cog successfully installed.").format(cog_name))
if cog.install_msg is not None: if cog.install_msg is not None:
await ctx.send(cog.install_msg) await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
@cog.command(name="uninstall") @cog.command(name="uninstall")
async def _cog_uninstall(self, ctx, cog_name: InstalledCog): async def _cog_uninstall(self, ctx, cog_name: InstalledCog):
@@ -381,11 +378,25 @@ class Downloader:
""" """
Lists all available cogs from a single repo. Lists all available cogs from a single repo.
""" """
installed = await self.installed_cogs()
installed_str = ""
if installed:
installed_str = _("Installed Cogs:\n") + "\n".join(
[
"- {}{}".format(i.name, ": {}".format(i.short) if i.short else "")
for i in installed
if i.repo_name == repo_name.name
]
)
cogs = repo_name.available_cogs cogs = repo_name.available_cogs
cogs = _("Available Cogs:\n") + "\n".join( cogs = _("Available Cogs:\n") + "\n".join(
["+ {}: {}".format(c.name, c.short or "") for c in cogs] [
"+ {}: {}".format(c.name, c.short or "")
for c in cogs
if not (c.hidden or c in installed)
]
) )
cogs = cogs + "\n\n" + installed_str
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"))
@@ -401,7 +412,9 @@ class Downloader:
) )
return return
msg = _("Information on {}:\n{}").format(cog.name, cog.description or "") msg = _("Information on {}:\n{}\n\nRequirements: {}").format(
cog.name, cog.description or "", ", ".join(cog.requirements) or "None"
)
await ctx.send(box(msg)) await ctx.send(box(msg))
async def is_installed( async def is_installed(

View File

@@ -17,6 +17,7 @@ class DownloaderException(Exception):
""" """
Base class for Downloader exceptions. Base class for Downloader exceptions.
""" """
pass pass
@@ -31,6 +32,7 @@ class InvalidRepoName(DownloaderException):
Throw when a repo name is invalid. Check Throw when a repo name is invalid. Check
the message for a more detailed reason. the message for a more detailed reason.
""" """
pass pass
@@ -39,6 +41,7 @@ class ExistingGitRepo(DownloaderException):
Thrown when trying to clone into a folder where a Thrown when trying to clone into a folder where a
git repo already exists. git repo already exists.
""" """
pass pass
@@ -47,6 +50,7 @@ class MissingGitRepo(DownloaderException):
Thrown when a git repo is expected to exist but Thrown when a git repo is expected to exist but
does not. does not.
""" """
pass pass
@@ -54,6 +58,7 @@ class CloningError(GitException):
""" """
Thrown when git clone returns a non zero exit code. Thrown when git clone returns a non zero exit code.
""" """
pass pass
@@ -62,6 +67,7 @@ class CurrentHashError(GitException):
Thrown when git returns a non zero exit code attempting Thrown when git returns a non zero exit code attempting
to determine the current commit hash. to determine the current commit hash.
""" """
pass pass
@@ -70,6 +76,7 @@ class HardResetError(GitException):
Thrown when there is an issue trying to execute a hard reset Thrown when there is an issue trying to execute a hard reset
(usually prior to a repo update). (usually prior to a repo update).
""" """
pass pass
@@ -77,6 +84,7 @@ class UpdateError(GitException):
""" """
Thrown when git pull returns a non zero error code. Thrown when git pull returns a non zero error code.
""" """
pass pass
@@ -84,6 +92,7 @@ class GitDiffError(GitException):
""" """
Thrown when a git diff fails. Thrown when a git diff fails.
""" """
pass pass
@@ -91,4 +100,5 @@ class PipError(DownloaderException):
""" """
Thrown when pip returns a non-zero return code. Thrown when pip returns a non-zero return code.
""" """
pass pass

View File

@@ -3,9 +3,8 @@ import distutils.dir_util
import shutil import shutil
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from typing import MutableMapping, Any from typing import MutableMapping, Any, TYPE_CHECKING
from redbot.core.utils import TYPE_CHECKING
from .log import log from .log import log
from .json_mixins import RepoJSONMixin from .json_mixins import RepoJSONMixin
@@ -76,6 +75,7 @@ class Installable(RepoJSONMixin):
self.bot_version = (3, 0, 0) self.bot_version = (3, 0, 0)
self.min_python_version = (3, 5, 1) self.min_python_version = (3, 5, 1)
self.hidden = False self.hidden = False
self.disabled = False
self.required_cogs = {} # Cog name -> repo URL self.required_cogs = {} # Cog name -> repo URL
self.requirements = () self.requirements = ()
self.tags = () self.tags = ()
@@ -117,7 +117,7 @@ class Installable(RepoJSONMixin):
try: try:
copy_func(src=str(self._location), dst=str(target_dir / self._location.stem)) copy_func(src=str(self._location), dst=str(target_dir / self._location.stem))
except: except:
log.exception("Error occurred when copying path:" " {}".format(self._location)) log.exception("Error occurred when copying path: {}".format(self._location))
return False return False
return True return True
@@ -146,9 +146,7 @@ class Installable(RepoJSONMixin):
info = json.load(f) info = json.load(f)
except json.JSONDecodeError: except json.JSONDecodeError:
info = {} info = {}
log.exception( log.exception("Invalid JSON information file at path: {}".format(info_file_path))
"Invalid JSON information file at path:" " {}".format(info_file_path)
)
else: else:
self._info = info self._info = info
@@ -176,6 +174,12 @@ class Installable(RepoJSONMixin):
hidden = False hidden = False
self.hidden = hidden self.hidden = hidden
try:
disabled = bool(info.get("disabled", False))
except ValueError:
disabled = False
self.disabled = disabled
self.required_cogs = info.get("required_cogs", {}) self.required_cogs = info.get("required_cogs", {})
self.requirements = info.get("requirements", ()) self.requirements = info.get("requirements", ())

View File

@@ -1,93 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: ../downloader.py:215
msgid "That git repo has already been added under another name."
msgstr ""
#: ../downloader.py:217 ../downloader.py:218
msgid "Something went wrong during the cloning process."
msgstr ""
#: ../downloader.py:220
msgid "Repo `{}` successfully added."
msgstr ""
#: ../downloader.py:229
msgid "The repo `{}` has been deleted successfully."
msgstr ""
#: ../downloader.py:237
msgid ""
"Installed Repos:\n"
msgstr ""
#: ../downloader.py:258
msgid "Error, there is no cog by the name of `{}` in the `{}` repo."
msgstr ""
#: ../downloader.py:263
msgid "Failed to install the required libraries for `{}`: `{}`"
msgstr ""
#: ../downloader.py:273
msgid "`{}` cog successfully installed."
msgstr ""
#: ../downloader.py:289
msgid "`{}` was successfully removed."
msgstr ""
#: ../downloader.py:291
msgid "That cog was installed but can no longer be located. You may need to remove it's files manually if it is still usable."
msgstr ""
#: ../downloader.py:315
msgid "Cog update completed successfully."
msgstr ""
#: ../downloader.py:323
msgid ""
"Available Cogs:\n"
msgstr ""
#: ../downloader.py:335
msgid "There is no cog `{}` in the repo `{}`"
msgstr ""
#: ../downloader.py:340
msgid ""
"Information on {}:\n"
"{}"
msgstr ""
#: ../downloader.py:381
msgid "Missing from info.json"
msgstr ""
#: ../downloader.py:390
msgid ""
"Command: {}\n"
"Made by: {}\n"
"Repo: {}\n"
"Cog name: {}"
msgstr ""
#: ../downloader.py:422
msgid "That command doesn't seem to exist."
msgstr ""

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../downloader.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

View File

@@ -2,17 +2,13 @@ import asyncio
import functools import functools
import os import os
import pkgutil import pkgutil
import shutil
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
from discord.ext import commands from redbot.core import data_manager, commands
from redbot.core import Config
from redbot.core import data_manager
from redbot.core.utils import safe_delete from redbot.core.utils import safe_delete
from .errors import * from .errors import *
from .installable import Installable, InstallableType from .installable import Installable, InstallableType
@@ -27,10 +23,8 @@ class Repo(RepoJSONMixin):
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 -q --ff-only"
GIT_DIFF_FILE_STATUS = ( GIT_DIFF_FILE_STATUS = "git -C {path} diff --no-commit-id --name-status {old_hash} {new_hash}"
"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"
PIP_INSTALL = "{python} -m pip install -U -t {target_dir} {reqs}" PIP_INSTALL = "{python} -m pip install -U -t {target_dir} {reqs}"
@@ -98,7 +92,7 @@ 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 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")
@@ -222,7 +216,7 @@ class Repo(RepoJSONMixin):
if p.returncode != 0: if p.returncode != 0:
raise GitException( raise GitException(
"Could not determine current branch" " at path: {}".format(self.folder_path) "Could not determine current branch at path: {}".format(self.folder_path)
) )
return p.stdout.decode().strip() return p.stdout.decode().strip()
@@ -472,7 +466,7 @@ class Repo(RepoJSONMixin):
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
return tuple( return tuple(
[m for m in self.available_modules if m.type == InstallableType.COG and not m.hidden] [m for m in self.available_modules if m.type == InstallableType.COG and not m.disabled]
) )
@property @property
@@ -495,7 +489,6 @@ class Repo(RepoJSONMixin):
class RepoManager: class RepoManager:
def __init__(self): def __init__(self):
self._repos = {} self._repos = {}

View File

@@ -9,7 +9,8 @@ import discord
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
from redbot.core import Config, bank, commands from redbot.core import Config, bank, commands
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import pagify, box from redbot.core.utils.chat_formatting import box
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
from redbot.core.bot import Red from redbot.core.bot import Red
@@ -74,7 +75,6 @@ SLOT_PAYOUTS_MSG = _(
def guild_only_check(): def guild_only_check():
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
if await bank.is_global(): if await bank.is_global():
return True return True
@@ -87,7 +87,6 @@ def guild_only_check():
class SetParser: class SetParser:
def __init__(self, argument): def __init__(self, argument):
allowed = ("+", "-") allowed = ("+", "-")
self.sum = int(argument) self.sum = int(argument)
@@ -139,11 +138,11 @@ class Economy:
self.config.register_role(**self.default_role_settings) self.config.register_role(**self.default_role_settings)
self.slot_register = defaultdict(dict) self.slot_register = defaultdict(dict)
@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""" """Bank operations"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@_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):
@@ -212,7 +211,6 @@ class Economy:
) )
@_bank.command() @_bank.command()
@guild_only_check()
@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""" """Deletes bank accounts"""
@@ -228,13 +226,13 @@ class Economy:
else: else:
await bank.wipe_bank() await bank.wipe_bank()
await ctx.send( await ctx.send(
_("All bank accounts for {} have been " "deleted.").format( _("All bank accounts for {} have been deleted.").format(
self.bot.user.name if await bank.is_global() else "this server" self.bot.user.name if await bank.is_global() else "this server"
) )
) )
@commands.command()
@guild_only_check() @guild_only_check()
@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
@@ -254,7 +252,7 @@ class Economy:
_( _(
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n" "{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
"You currently have {3} {1}.\n\n" "You currently have {3} {1}.\n\n"
"You are currently #{4} on the leaderboard!" "You are currently #{4} on the global leaderboard!"
).format( ).format(
author, author,
credits_name, credits_name,
@@ -267,7 +265,7 @@ class Economy:
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( _("{} Too soon. For your next payday you have to wait {}.").format(
author.mention, dtime author.mention, dtime
) )
) )
@@ -301,7 +299,7 @@ class Economy:
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( _("{} Too soon. For your next payday you have to wait {}.").format(
author.mention, dtime author.mention, dtime
) )
) )
@@ -312,8 +310,8 @@ class Economy:
"""Prints out the leaderboard """Prints out the leaderboard
Defaults to top 10""" Defaults to top 10"""
# Originally coded by Airenkun - edited by irdumb, rewritten by Palm__ for v3
guild = ctx.guild guild = ctx.guild
author = ctx.author
if top < 1: if top < 1:
top = 10 top = 10
if ( if (
@@ -323,25 +321,25 @@ class Economy:
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: if len(bank_sorted) < top:
top = len(bank_sorted) top = len(bank_sorted)
highscore = "" header = f"{f'#':4}{f'Name':36}{f'Score':2}\n"
for pos, acc in enumerate(bank_sorted, 1): highscores = [
pos = pos (
poswidth = 2 f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} "
name = acc[1]["name"] f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
namewidth = 35
balance = acc[1]["balance"]
balwidth = 2
highscore += "{pos: <{poswidth}} {name: <{namewidth}s} {balance: >{balwidth}}\n".format(
pos=pos,
poswidth=poswidth,
name=name,
namewidth=namewidth,
balance=balance,
balwidth=balwidth,
) )
if highscore != "": if acc[0] != author.id
for page in pagify(highscore, shorten_by=12): else (
await ctx.send(box(page, lang="py")) f"{f'{pos}.': <{3 if pos < 10 else 2}} <<{acc[1]['name'] + '>>': <{33}s} "
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
)
for pos, acc in enumerate(bank_sorted, 1)
]
if highscores:
pages = [
f"```md\n{header}{''.join(''.join(highscores[x:x + 10]))}```"
for x in range(0, len(highscores), 10)
]
await menu(ctx, pages, DEFAULT_CONTROLS)
else: else:
await ctx.send(_("There are no accounts in the bank.")) await ctx.send(_("There are no accounts in the bank."))
@@ -427,7 +425,7 @@ class Economy:
now = then - bid + pay now = then - bid + pay
await bank.set_balance(author, now) await bank.set_balance(author, now)
await channel.send( await channel.send(
_("{}\n{} {}\n\nYour bid: {}\n{}{}!" "").format( _("{}\n{} {}\n\nYour bid: {}\n{}{}!").format(
slot, author.mention, payout["phrase"], bid, then, now slot, author.mention, payout["phrase"], bid, then, now
) )
) )
@@ -436,7 +434,7 @@ class Economy:
await bank.withdraw_credits(author, bid) await bank.withdraw_credits(author, bid)
now = then - bid now = then - bid
await channel.send( await channel.send(
_("{}\n{} Nothing!\nYour bid: {}\n{}{}!" "").format( _("{}\n{} Nothing!\nYour bid: {}\n{}{}!").format(
slot, author.mention, bid, then, now slot, author.mention, bid, then, now
) )
) )
@@ -448,7 +446,6 @@ class Economy:
"""Changes economy module settings""" """Changes economy module settings"""
guild = ctx.guild guild = ctx.guild
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help()
if await bank.is_global(): if await bank.is_global():
slot_min = await self.config.SLOT_MIN() slot_min = await self.config.SLOT_MIN()
slot_max = await self.config.SLOT_MAX() slot_max = await self.config.SLOT_MAX()
@@ -497,7 +494,7 @@ class Economy:
"""Maximum slot machine bid""" """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 slotmax bid amount. Must be greater than slotmin."))
return return
guild = ctx.guild guild = ctx.guild
credits_name = await bank.get_currency_name(guild) credits_name = await bank.get_currency_name(guild)
@@ -526,9 +523,7 @@ class Economy:
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( _("Value modified. At least {} seconds must pass between each payday.").format(seconds)
seconds
)
) )
@economyset.command() @economyset.command()
@@ -543,7 +538,7 @@ 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 {} {}.").format(creds, 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):
@@ -555,7 +550,7 @@ class Economy:
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( _("Every payday will now give {} {} to people with the role {}.").format(
creds, credits_name, role.name creds, credits_name, role.name
) )
) )
@@ -569,7 +564,7 @@ class Economy:
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 {} {}.").format(creds, credits_name)
) )
# What would I ever do without stackoverflow? # What would I ever do without stackoverflow?

View File

@@ -1,199 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: ../economy.py:40
msgid "JACKPOT! 226! Your bid has been multiplied * 2500!"
msgstr ""
#: ../economy.py:44
msgid "4LC! +1000!"
msgstr ""
#: ../economy.py:48
msgid "Three cherries! +800!"
msgstr ""
#: ../economy.py:52
msgid "2 6! Your bid has been multiplied * 4!"
msgstr ""
#: ../economy.py:56
msgid "Two cherries! Your bid has been multiplied * 3!"
msgstr ""
#: ../economy.py:60
msgid "Three symbols! +500!"
msgstr ""
#: ../economy.py:64
msgid "Two consecutive symbols! Your bid has been multiplied * 2!"
msgstr ""
#: ../economy.py:68
msgid ""
"Slot machine payouts:\n"
"{two.value} {two.value} {six.value} Bet * 2500\n"
"{flc.value} {flc.value} {flc.value} +1000\n"
"{cherries.value} {cherries.value} {cherries.value} +800\n"
"{two.value} {six.value} Bet * 4\n"
"{cherries.value} {cherries.value} Bet * 3\n"
"\n"
"Three symbols: +500\n"
"Two symbols: Bet * 2"
msgstr ""
#: ../economy.py:157
msgid "{}'s balance is {} {}"
msgstr ""
#: ../economy.py:171
msgid "{} transferred {} {} to {}"
msgstr ""
#: ../economy.py:191
msgid "{} added {} {} to {}'s account."
msgstr ""
#: ../economy.py:196
msgid "{} removed {} {} from {}'s account."
msgstr ""
#: ../economy.py:201
msgid "{} set {}'s account to {} {}."
msgstr ""
#: ../economy.py:212
msgid ""
"This will delete all bank accounts for {}.\n"
"If you're sure, type `{}bank reset yes`"
msgstr ""
#: ../economy.py:229
msgid "All bank accounts of this guild have been deleted."
msgstr ""
#: ../economy.py:248 ../economy.py:268
msgid "{} Here, take some {}. Enjoy! (+{} {}!)"
msgstr ""
#: ../economy.py:258 ../economy.py:276
msgid "{} Too soon. For your next payday you have to wait {}."
msgstr ""
#: ../economy.py:313
msgid "There are no accounts in the bank."
msgstr ""
#: ../economy.py:339
msgid "You're on cooldown, try again in a bit."
msgstr ""
#: ../economy.py:342
msgid "That's an invalid bid amount, sorry :/"
msgstr ""
#: ../economy.py:345
msgid "You ain't got enough money, friend."
msgstr ""
#: ../economy.py:391
msgid ""
"{}\n"
"{} {}\n"
"\n"
"Your bid: {}\n"
"{} → {}!"
msgstr ""
#: ../economy.py:398
msgid ""
"{}\n"
"{} Nothing!\n"
"Your bid: {}\n"
"{} → {}!"
msgstr ""
#: ../economy.py:423
msgid ""
"Minimum slot bid: {}\n"
"Maximum slot bid: {}\n"
"Slot cooldown: {}\n"
"Payday amount: {}\n"
"Payday cooldown: {}\n"
"Amount given at account registration: {}"
msgstr ""
#: ../economy.py:433
msgid "Current Economy settings:"
msgstr ""
#: ../economy.py:441
msgid "Invalid min bid amount."
msgstr ""
#: ../economy.py:449
msgid "Minimum bid is now {} {}."
msgstr ""
#: ../economy.py:456
msgid "Invalid slotmax bid amount. Must be greater than slotmin."
msgstr ""
#: ../economy.py:465
msgid "Maximum bid is now {} {}."
msgstr ""
#: ../economy.py:475
msgid "Cooldown is now {} seconds."
msgstr ""
#: ../economy.py:485
msgid "Value modified. At least {} seconds must pass between each payday."
msgstr ""
#: ../economy.py:494
msgid "Har har so funny."
msgstr ""
#: ../economy.py:500
msgid "Every payday will now give {} {}."
msgstr ""
#: ../economy.py:511
msgid "Registering an account will now give {} {}."
msgstr ""
#: ../economy.py:517
msgid "weeks"
msgstr ""
#: ../economy.py:518
msgid "days"
msgstr ""
#: ../economy.py:519
msgid "hours"
msgstr ""
#: ../economy.py:520
msgid "minutes"
msgstr ""
#: ../economy.py:521
msgid "seconds"
msgstr ""

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../economy.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

View File

@@ -49,7 +49,6 @@ class Filter:
Using this command with no subcommands will send Using this command with no subcommands will send
the list of the server's filtered words.""" the list of the server's filtered words."""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help()
server = ctx.guild server = ctx.guild
author = ctx.author author = ctx.author
word_list = await self.settings.guild(server).filter() word_list = await self.settings.guild(server).filter()
@@ -124,36 +123,35 @@ class Filter:
@_filter.command(name="names") @_filter.command(name="names")
async def filter_names(self, ctx: commands.Context): async def filter_names(self, ctx: commands.Context):
""" """Toggles whether or not to check names and nicknames against the filter
Toggles whether or not to check names and nicknames against the filter
This is disabled by default This is disabled by default
""" """
guild = ctx.guild guild = ctx.guild
current_setting = await self.settings.guild(guild).filter_names() current_setting = await self.settings.guild(guild).filter_names()
await self.settings.guild(guild).filter_names.set(not current_setting) await self.settings.guild(guild).filter_names.set(not current_setting)
if current_setting: if current_setting:
await ctx.send( await ctx.send(_("Names and nicknames will no longer be checked against the filter."))
_("Names and nicknames will no longer be " "checked against the filter")
)
else: else:
await ctx.send(_("Names and nicknames will now be checked against " "the filter")) await ctx.send(_("Names and nicknames will now be checked against the filter."))
@_filter.command(name="defaultname") @_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
Sets the default name to use if filtering names is enabled
Note that this has no effect if filtering names is disabled Note that this has no effect if filtering names is disabled
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") @_filter.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
Sets up an autoban if the specified number of messages are
filtered in the specified amount of time (in seconds) The timeframe is represented by seconds.
""" """
if (count <= 0) != (timeframe <= 0): if (count <= 0) != (timeframe <= 0):
await ctx.send( await ctx.send(
@@ -223,7 +221,7 @@ 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:

View File

@@ -1,65 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: ../filter.py:62
msgid "Filtered in this server:"
msgstr ""
#: ../filter.py:67
msgid "I can't send direct messages to you."
msgstr ""
#: ../filter.py:96
msgid "Words added to filter."
msgstr ""
#: ../filter.py:98
msgid "Words already in the filter."
msgstr ""
#: ../filter.py:127
msgid "Words removed from filter."
msgstr ""
#: ../filter.py:129
msgid "Those words weren't in the filter."
msgstr ""
#: ../filter.py:142
msgid "Names and nicknames will no longer be checked against the filter"
msgstr ""
#: ../filter.py:147
msgid "Names and nicknames will now be checked against the filter"
msgstr ""
#: ../filter.py:160
msgid "The name to use on filtered names has been set"
msgstr ""
#: ../filter.py:171
msgid "Count and timeframe either both need to be 0 or both need to be greater than 0!"
msgstr ""
#: ../filter.py:179
msgid "Autoban disabled."
msgstr ""
#: ../filter.py:183
msgid "Count and time have been set."
msgstr ""

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../filter.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

View File

@@ -3,12 +3,11 @@ 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 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.chat_formatting import escape, italics, pagify from redbot.core.utils.chat_formatting import escape, italics, pagify
_ = Translator("General", __file__) _ = Translator("General", __file__)
@@ -21,7 +20,6 @@ class RPS(Enum):
class RPSParser: class RPSParser:
def __init__(self, argument): def __init__(self, argument):
argument = argument.lower() argument = argument.lower()
if argument == "rock": if argument == "rock":
@@ -98,7 +96,7 @@ class General:
msg = "" msg = ""
if user.id == ctx.bot.user.id: if user.id == ctx.bot.user.id:
user = ctx.author user = ctx.author
msg = _("Nice try. You think this is funny?\n" "How about *this* instead:\n\n") msg = _("Nice try. You think this is funny?\n How about *this* instead:\n\n")
char = "abcdefghijklmnopqrstuvwxyz" char = "abcdefghijklmnopqrstuvwxyz"
tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz" tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz"
table = str.maketrans(char, tran) table = str.maketrans(char, tran)
@@ -165,7 +163,9 @@ class General:
@commands.command() @commands.command()
async def lmgtfy(self, ctx, *, search_terms: str): async def lmgtfy(self, ctx, *, search_terms: str):
"""Creates a lmgtfy link""" """Creates a lmgtfy link"""
search_terms = escape(search_terms.replace(" ", "+"), mass_mentions=True) search_terms = escape(
search_terms.replace("+", "%2B").replace(" ", "+"), mass_mentions=True
)
await ctx.send("https://lmgtfy.com/?q={}".format(search_terms)) await ctx.send("https://lmgtfy.com/?q={}".format(search_terms))
@commands.command(hidden=True) @commands.command(hidden=True)
@@ -192,18 +192,12 @@ class General:
async def serverinfo(self, ctx): async def serverinfo(self, ctx):
"""Shows server's informations""" """Shows server's informations"""
guild = ctx.guild guild = ctx.guild
online = len( online = len([m.status for m in guild.members if m.status != discord.Status.offline])
[
m.status
for m in guild.members
if m.status == discord.Status.online or m.status == discord.Status.idle
]
)
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 {}. That's over {} days ago!").format(
guild.created_at.strftime("%d %b %Y %H:%M"), passed guild.created_at.strftime("%d %b %Y %H:%M"), passed
) )
@@ -228,52 +222,93 @@ class General:
try: try:
await ctx.send(embed=data) await ctx.send(embed=data)
except discord.HTTPException: except discord.HTTPException:
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, *, search_terms: str, definition_number: int = 1): async def urban(self, ctx, *, word):
"""Urban Dictionary search """Searches urban dictionary entries using the unofficial api"""
Definition number must be between 1 and 10"""
def encode(s):
return quote_plus(s, encoding="utf-8", errors="replace")
# definition_number is just there to show up in the help
# all this mess is to avoid forcing double quotes on the user
search_terms = search_terms.split(" ")
try: try:
if len(search_terms) > 1: url = "https://api.urbandictionary.com/v0/define?term=" + str(word).lower()
pos = int(search_terms[-1]) - 1
search_terms = search_terms[:-1] headers = {"content-type": "application/json"}
else:
pos = 0
if pos not in range(0, 11): # API only provides the
pos = 0 # top 10 definitions
except ValueError:
pos = 0
search_terms = {"term": "+".join([s for s in search_terms])}
url = "http://api.urbandictionary.com/v0/define"
try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url, params=search_terms) as r: async with session.get(url, headers=headers) as response:
result = await r.json() data = await response.json()
item_list = result["list"]
if item_list:
definition = item_list[pos]["definition"]
example = item_list[pos]["example"]
defs = len(item_list)
msg = "**Definition #{} out of {}:\n**{}\n\n" "**Example:\n**{}".format(
pos + 1, defs, definition, example
)
msg = pagify(msg, ["\n"])
for page in msg:
await ctx.send(page)
else:
await ctx.send(_("Your search terms gave no results."))
except IndexError:
await ctx.send(_("There is no definition #{}").format(pos + 1))
except: except:
await ctx.send(_("Error.")) await ctx.send(
_("No Urban dictionary entries were found or there was an error in the process")
)
if data.get("error") != 404:
if await ctx.embed_requested():
# a list of embeds
embeds = []
for ud in data["list"]:
embed = discord.Embed()
embed.title = _("{} by {}").format(ud["word"].capitalize(), ud["author"])
embed.url = ud["permalink"]
description = "{} \n \n **Example : ** {}".format(
ud["definition"], ud.get("example", "N/A")
)
if len(description) > 2048:
description = "{}...".format(description[:2045])
embed.description = description
embed.set_footer(
text=_("{} Down / {} Up , Powered by urban dictionary").format(
ud["thumbs_down"], ud["thumbs_up"]
)
)
embeds.append(embed)
if embeds is not None and len(embeds) > 0:
await menu(
ctx,
pages=embeds,
controls=DEFAULT_CONTROLS,
message=None,
page=0,
timeout=30,
)
else:
messages = []
for ud in data["list"]:
description = _("{} \n \n **Example : ** {}").format(
ud["definition"], ud.get("example", "N/A")
)
if len(description) > 2048:
description = "{}...".format(description[:2045])
description = description
message = _(
"<{}> \n {} by {} \n \n {} \n \n {} Down / {} Up, Powered by urban "
"dictionary"
).format(
ud["permalink"],
ud["word"].capitalize(),
ud["author"],
description,
ud["thumbs_down"],
ud["thumbs_up"],
)
messages.append(message)
if messages is not None and len(messages) > 0:
await menu(
ctx,
pages=messages,
controls=DEFAULT_CONTROLS,
message=None,
page=0,
timeout=30,
)
else:
await ctx.send(
_("No Urban dictionary entries were found or there was an error in the process")
)
return

View File

@@ -1,241 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: ../general.py:42
msgid "As I see it, yes"
msgstr ""
#: ../general.py:42
msgid "It is certain"
msgstr ""
#: ../general.py:42
msgid "It is decidedly so"
msgstr ""
#: ../general.py:43
msgid "Most likely"
msgstr ""
#: ../general.py:43
msgid "Outlook good"
msgstr ""
#: ../general.py:43
msgid "Signs point to yes"
msgstr ""
#: ../general.py:44
msgid "Without a doubt"
msgstr ""
#: ../general.py:44
msgid "Yes"
msgstr ""
#: ../general.py:44
msgid "Yes definitely"
msgstr ""
#: ../general.py:44
msgid "You may rely on it"
msgstr ""
#: ../general.py:45
msgid "Ask again later"
msgstr ""
#: ../general.py:45
msgid "Reply hazy, try again"
msgstr ""
#: ../general.py:46
msgid "Better not tell you now"
msgstr ""
#: ../general.py:46
msgid "Cannot predict now"
msgstr ""
#: ../general.py:47
msgid "Concentrate and ask again"
msgstr ""
#: ../general.py:47
msgid "Don't count on it"
msgstr ""
#: ../general.py:47
msgid "My reply is no"
msgstr ""
#: ../general.py:48
msgid "My sources say no"
msgstr ""
#: ../general.py:48
msgid "Outlook not so good"
msgstr ""
#: ../general.py:48
msgid "Very doubtful"
msgstr ""
#: ../general.py:64
msgid "Not enough choices to pick from."
msgstr ""
#: ../general.py:78
msgid "{} :game_die: {} :game_die:"
msgstr ""
#: ../general.py:81
msgid "{} Maybe higher than 1? ;P"
msgstr ""
#: ../general.py:93
msgid ""
"Nice try. You think this is funny?How about *this* instead:\n"
"\n"
msgstr ""
#: ../general.py:106
msgid "*flips a coin and... "
msgstr ""
#: ../general.py:106
msgid "HEADS!*"
msgstr ""
#: ../general.py:106
msgid "TAILS!*"
msgstr ""
#: ../general.py:130
msgid "{} You win {}!"
msgstr ""
#: ../general.py:134
msgid "{} You lose {}!"
msgstr ""
#: ../general.py:138
msgid "{} We're square {}!"
msgstr ""
#: ../general.py:151
msgid "That doesn't look like a question."
msgstr ""
#: ../general.py:159
msgid " Stopwatch started!"
msgstr ""
#: ../general.py:163
msgid " Stopwatch stopped! Time: **"
msgstr ""
#: ../general.py:216 ../general.py:217
msgid ""
"{}\n"
"({} days ago)"
msgstr ""
#: ../general.py:219
msgid "Chilling in {} status"
msgstr ""
#: ../general.py:223
msgid "Playing {}"
msgstr ""
#: ../general.py:225
msgid "Streaming [{}]({})"
msgstr ""
#: ../general.py:227
msgid "Listening to {}"
msgstr ""
#: ../general.py:229
msgid "Watching {}"
msgstr ""
#: ../general.py:234
msgid "None"
msgstr ""
#: ../general.py:237
msgid "Joined Discord on"
msgstr ""
#: ../general.py:238
msgid "Joined this guild on"
msgstr ""
#: ../general.py:239 ../general.py:286
msgid "Roles"
msgstr ""
#: ../general.py:240
msgid "Member #{} | User ID: {}"
msgstr ""
#: ../general.py:257 ../general.py:299
msgid "I need the `Embed links` permission to send this."
msgstr ""
#: ../general.py:272
msgid "Since {}. That's over {} days ago!"
msgstr ""
#: ../general.py:282
msgid "Region"
msgstr ""
#: ../general.py:283
msgid "Users"
msgstr ""
#: ../general.py:284
msgid "Text Channels"
msgstr ""
#: ../general.py:285
msgid "Voice Channels"
msgstr ""
#: ../general.py:287
msgid "Owner"
msgstr ""
#: ../general.py:288
msgid "Guild ID: "
msgstr ""
#: ../general.py:343
msgid "Your search terms gave no results."
msgstr ""
#: ../general.py:345
msgid "There is no definition #{}"
msgstr ""
#: ../general.py:347
msgid "Error."
msgstr ""

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../general.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

View File

@@ -13,6 +13,7 @@ GIPHY_API_KEY = "dc6zaTOxFJmzC"
@cog_i18n(_) @cog_i18n(_)
class Image: class Image:
"""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):
@@ -23,7 +24,7 @@ class Image:
self.imgur_base_url = "https://api.imgur.com/3/" self.imgur_base_url = "https://api.imgur.com/3/"
def __unload(self): def __unload(self):
self.session.close() self.session.detach()
@commands.group(name="imgur") @commands.group(name="imgur")
async def _imgur(self, ctx): async def _imgur(self, ctx):
@@ -31,8 +32,7 @@ class Image:
Make sure to set the client ID using Make sure to set the client ID using
[p]imgurcreds""" [p]imgurcreds"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@_imgur.command(name="search") @_imgur.command(name="search")
async def imgur_search(self, ctx, *, term: str): async def imgur_search(self, ctx, *, term: str):
@@ -42,7 +42,7 @@ class Image:
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( _("A client ID has not been set! Please set one with {}.").format(
"`{}imgurcreds`".format(ctx.prefix) "`{}imgurcreds`".format(ctx.prefix)
) )
) )
@@ -54,7 +54,7 @@ class Image:
if data["success"]: if data["success"]:
results = data["data"] results = data["data"]
if not results: if not results:
await ctx.send(_("Your search returned no results")) await ctx.send(_("Your search returned no results."))
return return
shuffle(results) shuffle(results)
msg = _("Search results...\n") msg = _("Search results...\n")
@@ -63,7 +63,7 @@ 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 {}.").format(data["status"]))
@_imgur.command(name="subreddit") @_imgur.command(name="subreddit")
async def imgur_subreddit( async def imgur_subreddit(
@@ -91,7 +91,7 @@ class Image:
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( _("A client ID has not been set! Please set one with {}.").format(
"`{}imgurcreds`".format(ctx.prefix) "`{}imgurcreds`".format(ctx.prefix)
) )
) )
@@ -116,12 +116,13 @@ 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 {}.").format(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 """Sets the imgur client id
You will need an account on Imgur to get this You will need an account on Imgur to get this
You can get these by visiting https://api.imgur.com/oauth2/addclient You can get these by visiting https://api.imgur.com/oauth2/addclient
@@ -130,7 +131,7 @@ class Image:
set the authorization callback url to 'https://localhost' set the authorization callback url to 'https://localhost'
leave the app website blank, enter a valid email address, and leave the app website blank, enter a valid email address, and
enter a description. Check the box for the captcha, then click Next. enter a description. Check the box for the captcha, then click Next.
Your client ID will be on the page that loads""" Your client ID will be on the page that loads."""
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(_("Set the imgur client id!"))
@@ -143,7 +144,7 @@ class Image:
await ctx.send_help() await ctx.send_help()
return return
url = "http://api.giphy.com/v1/gifs/search?&api_key={}&q={}" "".format( url = "http://api.giphy.com/v1/gifs/search?&api_key={}&q={}".format(
GIPHY_API_KEY, keywords GIPHY_API_KEY, keywords
) )
@@ -155,7 +156,7 @@ 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 API."))
@commands.command(pass_context=True, no_pm=True) @commands.command(pass_context=True, no_pm=True)
async def gifr(self, ctx, *keywords): async def gifr(self, ctx, *keywords):
@@ -166,7 +167,7 @@ class Image:
await ctx.send_help() await ctx.send_help()
return return
url = "http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}" "".format( url = "http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}".format(
GIPHY_API_KEY, keywords GIPHY_API_KEY, keywords
) )
@@ -178,4 +179,4 @@ 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 API."))

View File

@@ -1,46 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: ../image.py:49
msgid "Your search returned no results"
msgstr ""
#: ../image.py:52
msgid ""
"Search results...\n"
msgstr ""
#: ../image.py:58 ../image.py:100
msgid "Something went wrong. Error code is {}"
msgstr ""
#: ../image.py:70
msgid "Only 'new' and 'top' are a valid sort type."
msgstr ""
#: ../image.py:98 ../image.py:135 ../image.py:157
msgid "No results found."
msgstr ""
#: ../image.py:115
msgid "Set the imgur client id!"
msgstr ""
#: ../image.py:137 ../image.py:159
msgid "Error contacting the API"
msgstr ""

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../image.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

View File

@@ -1,9 +1,8 @@
from discord.ext import commands from redbot.core import commands
import discord import discord
def mod_or_voice_permissions(**perms): def mod_or_voice_permissions(**perms):
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
@@ -31,7 +30,6 @@ def mod_or_voice_permissions(**perms):
def admin_or_voice_permissions(**perms): def admin_or_voice_permissions(**perms):
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
@@ -54,7 +52,6 @@ def admin_or_voice_permissions(**perms):
def bot_has_voice_permissions(**perms): def bot_has_voice_permissions(**perms):
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
guild = ctx.guild guild = ctx.guild
for vc in guild.voice_channels: for vc in guild.voice_channels:

View File

@@ -1,286 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: ../mod.py:209
msgid "Role hierarchy will be checked when moderation commands are issued."
msgstr ""
#: ../mod.py:213
msgid "Role hierarchy will be ignored when moderation commands are issued."
msgstr ""
#: ../mod.py:228
msgid "Autoban for mention spam enabled. Anyone mentioning {} or more different people in a single message will be autobanned."
msgstr ""
#: ../mod.py:239
msgid "Autoban for mention spam disabled."
msgstr ""
#: ../mod.py:249
msgid "Messages repeated up to 3 times will be deleted."
msgstr ""
#: ../mod.py:253
msgid "Repeated messages will be ignored."
msgstr ""
#: ../mod.py:267
msgid "Command deleting disabled."
msgstr ""
#: ../mod.py:270
msgid "Delete delay set to {} seconds."
msgstr ""
#: ../mod.py:275
msgid "Bot will delete command messages after {} seconds. Set this value to -1 to stop deleting messages"
msgstr ""
#: ../mod.py:279
msgid "I will not delete command messages."
msgstr ""
#: ../mod.py:292
msgid "Users unbanned with {} will be reinvited."
msgstr ""
#: ../mod.py:295
msgid "Users unbanned with {} will not be reinvited."
msgstr ""
#: ../mod.py:309 ../mod.py:349 ../mod.py:506
msgid "I cannot let you do that. Self-harm is bad {}"
msgstr ""
#: ../mod.py:313 ../mod.py:353 ../mod.py:510
msgid "I cannot let you do that. You are not higher than the user in the role hierarchy."
msgstr ""
#: ../mod.py:323 ../mod.py:379 ../mod.py:570
msgid "I'm not allowed to do that."
msgstr ""
#: ../mod.py:327
msgid "Done. That felt good."
msgstr ""
#: ../mod.py:369
msgid "Invalid days. Must be between 0 and 7."
msgstr ""
#: ../mod.py:384
msgid "Done. It was about time."
msgstr ""
#: ../mod.py:413
msgid "User is already banned."
msgstr ""
#: ../mod.py:429
msgid "User not found. Have you provided the correct user ID?"
msgstr ""
#: ../mod.py:433
msgid "I lack the permissions to do this."
msgstr ""
#: ../mod.py:435
msgid "Done. The user will not be able to join this guild."
msgstr ""
#: ../mod.py:472
msgid "You have been temporarily banned from {} until {}. Here is an invite for when your ban expires: {}"
msgstr ""
#: ../mod.py:481
msgid "I can't do that for some reason."
msgstr ""
#: ../mod.py:483
msgid "Something went wrong while banning"
msgstr ""
#: ../mod.py:485
msgid "Done. Enough chaos for now"
msgstr ""
#: ../mod.py:525
msgid ""
"You have been banned and then unbanned as a quick way to delete your messages.\n"
"You can now join the guild again. {}"
msgstr ""
#: ../mod.py:537
msgid "My role is not high enough to softban that user."
msgstr ""
#: ../mod.py:553
msgid "Done. Enough chaos."
msgstr ""
#: ../mod.py:587
msgid "Couldn't find a user with that ID!"
msgstr ""
#: ../mod.py:593
msgid "It seems that user isn't banned!"
msgstr ""
#: ../mod.py:601
msgid "Something went wrong while attempting to unban that user"
msgstr ""
#: ../mod.py:604
msgid "Unbanned that user from this guild"
msgstr ""
#: ../mod.py:619
msgid ""
"You've been unbanned from {}.\n"
"Here is an invite for that guild: {}"
msgstr ""
#: ../mod.py:623
msgid ""
"I failed to send an invite to that user. Perhaps you may be able to send it for me?\n"
"Here's the invite link: {}"
msgstr ""
#: ../mod.py:629
msgid "Something went wrong when attempting to send that useran invite. Here's the link so you can try: {}"
msgstr ""
#: ../mod.py:673 ../mod.py:710
msgid "No voice state for that user!"
msgstr ""
#: ../mod.py:687
msgid "That user is already muted and deafened guild-wide!"
msgstr ""
#: ../mod.py:690
msgid "User has been banned from speaking or listening in voice channels"
msgstr ""
#: ../mod.py:722
msgid "That user isn't muted or deafened by the guild!"
msgstr ""
#: ../mod.py:725
msgid "User is now allowed to speak and listen in voice channels"
msgstr ""
#: ../mod.py:754
msgid "I cannot do that, I lack the '{}' permission."
msgstr ""
#: ../mod.py:783
msgid "Muted {}#{} in channel {}"
msgstr ""
#: ../mod.py:797
msgid "That user is already muted in {}!"
msgstr ""
#: ../mod.py:800 ../mod.py:932
msgid "That user is not in a voice channel right now!"
msgstr ""
#: ../mod.py:802 ../mod.py:934
msgid "No voice state for the target!"
msgstr ""
#: ../mod.py:822
msgid "User has been muted in this channel."
msgstr ""
#: ../mod.py:858
msgid "User has been muted in this guild."
msgstr ""
#: ../mod.py:919
msgid "Unmuted {}#{} in channel {}"
msgstr ""
#: ../mod.py:929
msgid "That user is already unmuted in {}!"
msgstr ""
#: ../mod.py:949
msgid "User unmuted in this channel."
msgstr ""
#: ../mod.py:958
msgid "Unmute failed. Reason: {}"
msgstr ""
#: ../mod.py:981
msgid "User has been unmuted in this guild."
msgstr ""
#: ../mod.py:1045
msgid "Channel added to ignore list."
msgstr ""
#: ../mod.py:1047
msgid "Channel already in ignore list."
msgstr ""
#: ../mod.py:1055
msgid "This guild has been added to the ignore list."
msgstr ""
#: ../mod.py:1057
msgid "This guild is already being ignored."
msgstr ""
#: ../mod.py:1078
msgid "Channel removed from ignore list."
msgstr ""
#: ../mod.py:1080
msgid "That channel is not in the ignore list."
msgstr ""
#: ../mod.py:1088
msgid "This guild has been removed from the ignore list."
msgstr ""
#: ../mod.py:1090
msgid "This guild is not in the ignore list."
msgstr ""
#: ../mod.py:1102
msgid ""
"Currently ignoring:\n"
"{} channels\n"
"{} guilds\n"
msgstr ""
#: ../mod.py:1133
msgid "**Past 20 names**:"
msgstr ""
#: ../mod.py:1140
msgid "**Past 20 nicknames**:"
msgstr ""
#: ../mod.py:1146
msgid "That user doesn't have any recorded name or nickname change."
msgstr ""

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../mod.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

View File

@@ -12,6 +12,8 @@ from .checks import mod_or_voice_permissions, admin_or_voice_permissions, bot_ha
from redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, get_audit_reason from redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, get_audit_reason
from .log import log from .log import log
from redbot.core.utils.common_filters import filter_invites
_ = Translator("Mod", __file__) _ = Translator("Mod", __file__)
@@ -168,8 +170,6 @@ class Mod:
"""Manages server administration settings.""" """Manages server administration settings."""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
guild = ctx.guild guild = ctx.guild
await ctx.send_help()
# Display current settings # Display current settings
delete_repeats = await self.settings.guild(guild).delete_repeats() delete_repeats = await self.settings.guild(guild).delete_repeats()
ban_mention_spam = await self.settings.guild(guild).ban_mention_spam() ban_mention_spam = await self.settings.guild(guild).ban_mention_spam()
@@ -199,12 +199,12 @@ class Mod:
if not toggled: if not toggled:
await self.settings.guild(guild).respect_hierarchy.set(True) await self.settings.guild(guild).respect_hierarchy.set(True)
await ctx.send( await ctx.send(
_("Role hierarchy will be checked when " "moderation commands are issued.") _("Role hierarchy will be checked when moderation commands are issued.")
) )
else: else:
await self.settings.guild(guild).respect_hierarchy.set(False) await self.settings.guild(guild).respect_hierarchy.set(False)
await ctx.send( await ctx.send(
_("Role hierarchy will be ignored when " "moderation commands are issued.") _("Role hierarchy will be ignored when moderation commands are issued.")
) )
@modset.command() @modset.command()
@@ -241,7 +241,7 @@ class Mod:
cur_setting = await self.settings.guild(guild).delete_repeats() cur_setting = await self.settings.guild(guild).delete_repeats()
if not cur_setting: if not cur_setting:
await self.settings.guild(guild).delete_repeats.set(True) await self.settings.guild(guild).delete_repeats.set(True)
await ctx.send(_("Messages repeated up to 3 times will " "be deleted.")) await ctx.send(_("Messages repeated up to 3 times will be deleted."))
else: else:
await self.settings.guild(guild).delete_repeats.set(False) await self.settings.guild(guild).delete_repeats.set(False)
await ctx.send(_("Repeated messages will be ignored.")) await ctx.send(_("Repeated messages will be ignored."))
@@ -304,7 +304,7 @@ class Mod:
if author == user: if author == user:
await ctx.send( await ctx.send(
_("I cannot let you do that. Self-harm is " "bad {}").format("\N{PENSIVE FACE}") _("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}")
) )
return return
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
@@ -316,6 +316,9 @@ class Mod:
) )
) )
return return
elif ctx.guild.me.top_role <= user.top_role or user == ctx.guild.owner:
await ctx.send(_("I cannot do that due to discord hierarchy rules"))
return
audit_reason = get_audit_reason(author, reason) audit_reason = get_audit_reason(author, reason)
try: try:
await guild.kick(user, reason=audit_reason) await guild.kick(user, reason=audit_reason)
@@ -357,7 +360,7 @@ class Mod:
if author == user: if author == user:
await ctx.send( await ctx.send(
_("I cannot let you do that. Self-harm is " "bad {}").format("\N{PENSIVE FACE}") _("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}")
) )
return return
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
@@ -369,6 +372,9 @@ class Mod:
) )
) )
return return
elif ctx.guild.me.top_role <= user.top_role or user == ctx.guild.owner:
await ctx.send(_("I cannot do that due to discord hierarchy rules"))
return
if days: if days:
if days.isdigit(): if days.isdigit():
@@ -451,15 +457,15 @@ class Mod:
self.ban_queue.append(queue_entry) self.ban_queue.append(queue_entry)
try: try:
await guild.ban(user, reason=audit_reason) await guild.ban(user, reason=audit_reason)
log.info("{}({}) hackbanned {}" "".format(author.name, author.id, user_id)) log.info("{}({}) hackbanned {}".format(author.name, author.id, user_id))
except discord.NotFound: except discord.NotFound:
self.ban_queue.remove(queue_entry) self.ban_queue.remove(queue_entry)
await ctx.send(_("User not found. Have you provided the " "correct user ID?")) await ctx.send(_("User not found. Have you provided the correct user ID?"))
except discord.Forbidden: except discord.Forbidden:
self.ban_queue.remove(queue_entry) self.ban_queue.remove(queue_entry)
await ctx.send(_("I lack the permissions to do this.")) await ctx.send(_("I lack the permissions to do this."))
else: else:
await ctx.send(_("Done. The user will not be able to join this " "server.")) await ctx.send(_("Done. The user will not be able to join this server."))
user_info = await self.bot.get_user_info(user_id) user_info = await self.bot.get_user_info(user_id)
try: try:
@@ -547,7 +553,7 @@ class Mod:
if author == user: if author == user:
await ctx.send( await ctx.send(
_("I cannot let you do that. Self-harm is " "bad {}").format("\N{PENSIVE FACE}") _("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}")
) )
return return
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
@@ -624,7 +630,6 @@ class Mod:
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True)
@commands.bot_has_permissions(ban_members=True)
async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = None): async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = None):
"""Unbans the target user. """Unbans the target user.
@@ -632,13 +637,17 @@ class Mod:
1. Copy it from the mod log case (if one was created), or 1. Copy it from the mod log case (if one was created), or
2. enable developer mode, go to Bans in this server's settings, right- 2. enable developer mode, go to Bans in this server's settings, right-
click the user and select 'Copy ID'.""" click the user and select 'Copy ID'."""
channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).ban_members:
await ctx.send("I need the Ban Members permission to do this.")
return
guild = ctx.guild guild = ctx.guild
author = ctx.author author = ctx.author
user = await self.bot.get_user_info(user_id) user = await self.bot.get_user_info(user_id)
if not user: if not user:
await ctx.send(_("Couldn't find a user with that ID!")) await ctx.send(_("Couldn't find a user with that ID!"))
return return
reason = get_audit_reason(ctx.author, reason) audit_reason = get_audit_reason(ctx.author, reason)
bans = await guild.bans() bans = await guild.bans()
bans = [be.user for be in bans] bans = [be.user for be in bans]
if user not in bans: if user not in bans:
@@ -647,7 +656,7 @@ class Mod:
queue_entry = (guild.id, user.id) queue_entry = (guild.id, user.id)
self.unban_queue.append(queue_entry) self.unban_queue.append(queue_entry)
try: try:
await guild.unban(user, reason=reason) await guild.unban(user, reason=audit_reason)
except discord.HTTPException: except discord.HTTPException:
self.unban_queue.remove(queue_entry) self.unban_queue.remove(queue_entry)
await ctx.send(_("Something went wrong while attempting to unban that user")) await ctx.send(_("Something went wrong while attempting to unban that user"))
@@ -753,7 +762,7 @@ class Mod:
else: else:
await ctx.send(_("That user is already muted and deafened server-wide!")) await ctx.send(_("That user is already muted and deafened server-wide!"))
return return
await ctx.send(_("User has been banned from speaking or " "listening in voice channels")) await ctx.send(_("User has been banned from speaking or listening in voice channels"))
try: try:
await modlog.create_case( await modlog.create_case(
@@ -825,7 +834,7 @@ class Mod:
await ctx.send("Done.") await ctx.send("Done.")
except discord.Forbidden: except discord.Forbidden:
await ctx.send( await ctx.send(
_("I cannot do that, I lack the " "'{}' permission.").format("Manage Nicknames") _("I cannot do that, I lack the '{}' permission.").format("Manage Nicknames")
) )
@commands.group() @commands.group()
@@ -833,8 +842,7 @@ class Mod:
@checks.mod_or_permissions(manage_channel=True) @checks.mod_or_permissions(manage_channel=True)
async def mute(self, ctx: commands.Context): async def mute(self, ctx: commands.Context):
"""Mutes user in the channel/server""" """Mutes user in the channel/server"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@mute.command(name="voice") @mute.command(name="voice")
@commands.guild_only() @commands.guild_only()
@@ -1002,8 +1010,7 @@ class Mod:
"""Unmutes user in the channel/server """Unmutes user in the channel/server
Defaults to channel""" Defaults to channel"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@unmute.command(name="voice") @unmute.command(name="voice")
@commands.guild_only() @commands.guild_only()
@@ -1168,7 +1175,6 @@ class Mod:
async def ignore(self, ctx: commands.Context): async def ignore(self, ctx: commands.Context):
"""Adds servers/channels to ignorelist""" """Adds servers/channels to ignorelist"""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help()
await ctx.send(await self.count_ignored()) await ctx.send(await self.count_ignored())
@ignore.command(name="channel") @ignore.command(name="channel")
@@ -1185,7 +1191,7 @@ class Mod:
await ctx.send(_("Channel already in ignore list.")) await ctx.send(_("Channel already in ignore list."))
@ignore.command(name="server", aliases=["guild"]) @ignore.command(name="server", aliases=["guild"])
@commands.has_permissions(manage_guild=True) @checks.admin_or_permissions(manage_guild=True)
async def ignore_guild(self, ctx: commands.Context): async def ignore_guild(self, ctx: commands.Context):
"""Ignores current server""" """Ignores current server"""
guild = ctx.guild guild = ctx.guild
@@ -1201,7 +1207,6 @@ class Mod:
async def unignore(self, ctx: commands.Context): async def unignore(self, ctx: commands.Context):
"""Removes servers/channels from ignorelist""" """Removes servers/channels from ignorelist"""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help()
await ctx.send(await self.count_ignored()) await ctx.send(await self.count_ignored())
@unignore.command(name="channel") @unignore.command(name="channel")
@@ -1219,7 +1224,7 @@ class Mod:
await ctx.send(_("That channel is not in the ignore list.")) await ctx.send(_("That channel is not in the ignore list."))
@unignore.command(name="server", aliases=["guild"]) @unignore.command(name="server", aliases=["guild"])
@commands.has_permissions(manage_guild=True) @checks.admin_or_permissions(manage_guild=True)
async def unignore_guild(self, ctx: commands.Context): async def unignore_guild(self, ctx: commands.Context):
"""Removes current guild from ignore list""" """Removes current guild from ignore list"""
guild = ctx.message.guild guild = ctx.message.guild
@@ -1263,7 +1268,14 @@ class Mod:
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
async def userinfo(self, ctx, *, user: discord.Member = None): async def userinfo(self, ctx, *, user: discord.Member = None):
"""Shows users's informations""" """Shows information for a user.
This includes fields for status, discord join date, server
join date, voice state and previous names/nicknames.
If the user has none of roles, previous names or previous
nicknames, these fields will be omitted.
"""
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
@@ -1303,26 +1315,30 @@ class Mod:
if roles: if roles:
roles = ", ".join([x.name for x in roles]) roles = ", ".join([x.name for x in roles])
else: else:
roles = _("None") roles = None
data = discord.Embed(description=activity, colour=user.colour) data = discord.Embed(description=activity, colour=user.colour)
data.add_field(name=_("Joined Discord on"), value=created_on) data.add_field(name=_("Joined Discord on"), value=created_on)
data.add_field(name=_("Joined this server on"), value=joined_on) data.add_field(name=_("Joined this server on"), value=joined_on)
if roles is not None:
data.add_field(name=_("Roles"), value=roles, inline=False) data.add_field(name=_("Roles"), value=roles, inline=False)
if names: if names:
data.add_field(name=_("Previous Names"), value=", ".join(names), inline=False) val = filter_invites(", ".join(names))
data.add_field(name=_("Previous Names"), value=val, inline=False)
if nicks: if nicks:
data.add_field(name=_("Previous Nicknames"), value=", ".join(nicks), inline=False) val = filter_invites(", ".join(nicks))
data.add_field(name=_("Previous Nicknames"), value=val, inline=False)
if voice_state and voice_state.channel: if voice_state and voice_state.channel:
data.add_field( data.add_field(
name=_("Current voice channel"), name=_("Current voice channel"),
value="{0.name} (ID {0.id})".format(voice_state.channel), value="{0.name} (ID {0.id})".format(voice_state.channel),
inline=False, inline=False,
) )
data.set_footer(text=_("Member #{} | User ID: {}" "").format(member_number, user.id)) data.set_footer(text=_("Member #{} | User ID: {}").format(member_number, user.id))
name = str(user) name = str(user)
name = " ~ ".join((name, user.nick)) if user.nick else name name = " ~ ".join((name, user.nick)) if user.nick else name
name = filter_invites(name)
if user.avatar: if user.avatar:
avatar = user.avatar_url avatar = user.avatar_url
@@ -1335,7 +1351,7 @@ class Mod:
try: try:
await ctx.send(embed=data) await ctx.send(embed=data)
except discord.HTTPException: except discord.HTTPException:
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 names(self, ctx: commands.Context, user: discord.Member): async def names(self, ctx: commands.Context, user: discord.Member):
@@ -1355,15 +1371,15 @@ class Mod:
if msg: if msg:
await ctx.send(msg) await ctx.send(msg)
else: else:
await ctx.send(_("That user doesn't have any recorded name or " "nickname change.")) await ctx.send(_("That user doesn't have any recorded name or nickname change."))
async def get_names_and_nicks(self, user): async def get_names_and_nicks(self, user):
names = await self.settings.user(user).past_names() names = await self.settings.user(user).past_names()
nicks = await self.settings.member(user).past_nicks() nicks = await self.settings.member(user).past_nicks()
if names: if names:
names = [escape(name, mass_mentions=True) for name in names] names = [escape(name, mass_mentions=True) for name in names if name]
if nicks: if nicks:
nicks = [escape(nick, mass_mentions=True) for nick in nicks] nicks = [escape(nick, mass_mentions=True) for nick in nicks if nick]
return names, nicks return names, nicks
async def check_tempban_expirations(self): async def check_tempban_expirations(self):
@@ -1418,7 +1434,7 @@ class Mod:
await guild.ban(author, reason="Mention spam (Autoban)") await guild.ban(author, reason="Mention spam (Autoban)")
except discord.HTTPException: except discord.HTTPException:
log.info( log.info(
"Failed to ban member for mention spam in " "server {}.".format(guild.id) "Failed to ban member for mention spam in server {}.".format(guild.id)
) )
else: else:
try: try:
@@ -1439,7 +1455,13 @@ class Mod:
return True return True
return False return False
async def on_command(self, ctx: commands.Context): async def on_command_completion(self, ctx: commands.Context):
await self._delete_delay(ctx)
async def on_command_error(self, ctx: commands.Context, error):
await self._delete_delay(ctx)
async def _delete_delay(self, ctx: commands.Context):
"""Currently used for: """Currently used for:
* delete delay""" * delete delay"""
guild = ctx.guild guild = ctx.guild
@@ -1595,18 +1617,22 @@ class Mod:
if entry.target == target: if entry.target == target:
return entry return entry
async def on_member_update(self, before, after): async def on_member_update(self, before: discord.Member, after: discord.Member):
if before.name != after.name: if before.name != after.name:
async with self.settings.user(before).past_names() as name_list: async with self.settings.user(before).past_names() as name_list:
if after.nick in name_list: while None in name_list: # clean out null entries from a bug
name_list.remove(None)
if after.name in name_list:
# Ensure order is maintained without duplicates occuring # Ensure order is maintained without duplicates occuring
name_list.remove(after.nick) name_list.remove(after.name)
name_list.append(after.nick) name_list.append(after.name)
while len(name_list) > 20: while len(name_list) > 20:
name_list.pop(0) name_list.pop(0)
if before.nick != after.nick and after.nick is not None: if before.nick != after.nick and after.nick is not None:
async with self.settings.member(before).past_nicks() as nick_list: async with self.settings.member(before).past_nicks() as nick_list:
while None in nick_list: # clean out null entries from a bug
nick_list.remove(None)
if after.nick in nick_list: if after.nick in nick_list:
nick_list.remove(after.nick) nick_list.remove(after.nick)
nick_list.append(after.nick) nick_list.append(after.nick)

View File

@@ -1,61 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: ../modlog.py:36
msgid "Mod events will be sent to {}"
msgstr ""
#: ../modlog.py:42
msgid "I do not have permissions to send messages in {}!"
msgstr ""
#: ../modlog.py:52
msgid "Mod log deactivated."
msgstr ""
#: ../modlog.py:63
msgid "Current settings:"
msgstr ""
#: ../modlog.py:75
msgid "That action is not registered"
msgstr ""
#: ../modlog.py:82
msgid "Case creation for {} actions is now {}."
msgstr ""
#: ../modlog.py:94
msgid "Cases have been reset."
msgstr ""
#: ../modlog.py:103
msgid "That case does not exist for that guild"
msgstr ""
#: ../modlog.py:122
msgid "That case does not exist!"
msgstr ""
#: ../modlog.py:146
msgid "You are not authorized to modify that case!"
msgstr ""
#: ../modlog.py:155
msgid "Reason has been updated."
msgstr ""

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../modlog.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

View File

@@ -19,8 +19,7 @@ class ModLog:
@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""" """Settings for the mod log"""
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@modlogset.command() @modlogset.command()
@commands.guild_only() @commands.guild_only()
@@ -35,9 +34,7 @@ class ModLog:
await ctx.send(_("Mod events will be sent to {}").format(channel.mention)) await ctx.send(_("Mod events will be sent to {}").format(channel.mention))
else: else:
await ctx.send( await ctx.send(
_("I do not have permissions to " "send messages in {}!").format( _("I do not have permissions to send messages in {}!").format(channel.mention)
channel.mention
)
) )
else: else:
try: try:
@@ -98,19 +95,29 @@ class ModLog:
await ctx.send(_("That case does not exist for that server")) await ctx.send(_("That case does not exist for that server"))
return return
else: else:
await ctx.send(embed=await case.get_case_msg_content()) if await ctx.embed_requested():
await ctx.send(embed=await case.message_content(embed=True))
else:
await ctx.send(await case.message_content(embed=False))
@commands.command() @commands.command(usage="[case] <reason>")
@commands.guild_only() @commands.guild_only()
async def reason(self, ctx: commands.Context, case: int, *, reason: str = ""): async def reason(self, ctx: commands.Context, *, reason: str):
"""Lets you specify a reason for mod-log's cases """Lets you specify a reason for mod-log's cases
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 the server owner.
If no number is specified, the latest case will be used."""
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
if not reason: potential_case = reason.split()[0]
await ctx.send_help() if potential_case.isdigit():
return case = int(potential_case)
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:

View File

@@ -3,7 +3,6 @@ from typing import Tuple
class CogOrCommand(commands.Converter): class CogOrCommand(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> Tuple[str]: async def convert(self, ctx: commands.Context, arg: str) -> Tuple[str]:
ret = ctx.bot.get_cog(arg) ret = ctx.bot.get_cog(arg)
if ret: if ret:
@@ -12,15 +11,34 @@ class CogOrCommand(commands.Converter):
if ret: if ret:
return "commands", ret.qualified_name return "commands", ret.qualified_name
raise commands.BadArgument() raise commands.BadArgument(
'Cog or command "{arg}" not found. Please note that this is case sensitive.'
"".format(arg=arg)
)
class RuleType(commands.Converter): class RuleType(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> str: 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 "allow" return "allow"
if arg.lower() in ("deny", "blacklist", "denied"): if arg.lower() in ("deny", "blacklist", "denied"):
return "deny" return "deny"
raise commands.BadArgument() raise commands.BadArgument(
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny"'.format(arg=arg)
)
class ClearableRuleType(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> str:
if arg.lower() in ("allow", "whitelist", "allowed"):
return "allow"
if arg.lower() in ("deny", "blacklist", "denied"):
return "deny"
if arg.lower() in ("clear", "reset"):
return "clear"
raise commands.BadArgument(
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny", or "clear" to remove the rule'
"".format(arg=arg)
)

View File

@@ -0,0 +1,102 @@
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

View File

@@ -7,16 +7,19 @@ from redbot.core.bot import Red
from redbot.core import checks from redbot.core import checks
from redbot.core.config import Config from redbot.core.config import Config
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.caching import LRUDict
from .resolvers import val_if_check_is_valid, resolve_models from .resolvers import val_if_check_is_valid, resolve_models, entries_from_ctx
from .yaml_handler import yamlset_acl, yamlget_acl from .yaml_handler import yamlset_acl, yamlget_acl
from .converters import CogOrCommand, RuleType from .converters import CogOrCommand, RuleType, ClearableRuleType
from .mass_resolution import mass_resolve
_models = ["owner", "guildowner", "admin", "mod"] _models = ["owner", "guildowner", "admin", "mod", "all"]
_ = Translator("Permissions", __file__) _ = Translator("Permissions", __file__)
REACTS = {"\N{WHITE HEAVY CHECK MARK}": True, "\N{NEGATIVE SQUARED CROSS MARK}": False} REACTS = {"\N{WHITE HEAVY CHECK MARK}": True, "\N{NEGATIVE SQUARED CROSS MARK}": False}
Y_OR_N = {"y": True, "yes": True, "n": False, "no": False}
@cog_i18n(_) @cog_i18n(_)
@@ -32,61 +35,34 @@ class Permissions:
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.bot = bot self.bot = bot
self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True) self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True)
self._before = []
self._after = []
self.config.register_global(owner_models={}) self.config.register_global(owner_models={})
self.config.register_guild(owner_models={}) self.config.register_guild(owner_models={})
self.cache = LRUDict(size=25000) # This can be tuned later
def add_check(self, check_obj: object, before_or_after: str): async def get_user_ctx_overrides(self, ctx: commands.Context) -> dict:
""" """
adds a check to the check ordering This takes a context object, and returns a dict of
checks should be a function taking 2 arguments: allowed: list of commands
ctx: commands.Context denied: list of commands
level: str default: list of commands
and returning: representing how permissions interacts with the
None: do not interfere user, channel, guild, and (possibly) voice channel
True: command should be allowed even if they dont for all commands on the bot (not just the one in the context object)
have role or perm requirements for the check
False: command should be blocked
before_or_after: This mainly exists for use by the help formatter,
Should literally be a str equaling 'before' or 'after' but others may find it useful
This should be based on if this should take priority
over set rules or not
3rd party cogs adding checks using this should only allow Unlike the rest of the permission system, if other models are added later,
the owner to add checks before, and ensure only the owner due to optimizations made for this, this needs to be adjusted accordingly
can add checks recieving the level 'owner'
3rd party cogs should keep a copy of of any checks they registered This does not account for before and after permission hooks,
and deregister then on unload these need to be checked seperately
""" """
return await mass_resolve(ctx=ctx, config=self.config)
if before_or_after == "before": async def __global_check(self, ctx: commands.Context) -> bool:
self._before.append(check_obj)
elif before_or_after == "after":
self._after.append(check_obj)
else:
raise TypeError("RTFM")
def remove_check(self, check_obj: object, before_or_after: str):
"""
removes a previously registered check object
3rd party cogs should keep a copy of of any checks they registered
and deregister then on unload
"""
if before_or_after == "before":
self._before.remove(check_obj)
elif before_or_after == "after":
self._after.remove(check_obj)
else:
raise TypeError("RTFM")
async def __global_check(self, ctx):
""" """
Yes, this is needed on top of hooking into checks.py Yes, this is needed on top of hooking into checks.py
to ensure that unchecked commands can still be managed by permissions to ensure that unchecked commands can still be managed by permissions
@@ -94,7 +70,7 @@ class Permissions:
defering to check logic defering to check logic
This works since all checks must be True to run This works since all checks must be True to run
""" """
v = await self.check_overrides(ctx, "mod") v = await self.check_overrides(ctx, "all")
if v is False: if v is False:
return False return False
@@ -109,7 +85,7 @@ class Permissions:
ctx: `redbot.core.context.commands.Context` ctx: `redbot.core.context.commands.Context`
The context of the command The context of the command
level: `str` level: `str`
One of 'owner', 'guildowner', 'admin', 'mod' One of 'owner', 'guildowner', 'admin', 'mod', 'all'
Returns Returns
------- -------
@@ -119,25 +95,44 @@ class Permissions:
""" """
if await ctx.bot.is_owner(ctx.author): if await ctx.bot.is_owner(ctx.author):
return True return True
voice_channel = None
with contextlib.suppress(Exception):
voice_channel = ctx.author.voice.voice_channel
entries = [x 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])
for check in self._before: before = [
getattr(cog, "_{0.__class__.__name__}__red_permissions_before".format(cog), None)
for cog in ctx.bot.cogs.values()
]
for check in before:
if check is None:
continue
override = await val_if_check_is_valid(check=check, ctx=ctx, level=level) override = await val_if_check_is_valid(check=check, ctx=ctx, level=level)
if override is not None: if override is not None:
return override return override
# checked ids + configureable to be checked against
cache_tup = entries_from_ctx(ctx) + (
ctx.cog.__class__.__name__,
ctx.command.qualified_name,
)
if cache_tup in self.cache:
override = self.cache[cache_tup]
if override is not None:
return override
else:
for model in self.resolution_order[level]: for model in self.resolution_order[level]:
if ctx.guild is None and model != "owner":
break
override_model = getattr(self, model + "_model", None) override_model = getattr(self, model + "_model", None)
override = await override_model(ctx) if override_model else None override = await override_model(ctx) if override_model else None
if override is not None: if override is not None:
self.cache[cache_tup] = override
return override return override
# This is intentional not being in an else block
self.cache[cache_tup] = None
for check in self._after: after = [
getattr(cog, "_{0.__class__.__name__}__red_permissions_after".format(cog), None)
for cog in ctx.bot.cogs.values()
]
for check in after:
override = await val_if_check_is_valid(check=check, ctx=ctx, level=level) override = await val_if_check_is_valid(check=check, ctx=ctx, level=level)
if override is not None: if override is not None:
return override return override
@@ -156,7 +151,8 @@ class Permissions:
""" """
Handles guild level overrides Handles guild level overrides
""" """
if ctx.guild is None:
return None
async with self.config.guild(ctx.guild).owner_models() as models: async with self.config.guild(ctx.guild).owner_models() as models:
return resolve_models(ctx=ctx, models=models) return resolve_models(ctx=ctx, models=models)
@@ -171,8 +167,7 @@ class Permissions:
""" """
Permission management tools Permission management tools
""" """
if ctx.invoked_subcommand is None: pass
await ctx.send_help()
@permissions.command() @permissions.command()
async def explain(self, ctx: commands.Context): async def explain(self, ctx: commands.Context):
@@ -206,10 +201,10 @@ class Permissions:
"\n" "\n"
"1. Rules about a user.\n" "1. Rules about a user.\n"
"2. Rules about the voice channel a user is in.\n" "2. Rules about the voice channel a user is in.\n"
"3. Rules about the text channel a command was issued in\n" "3. Rules about the text channel a command was issued in.\n"
"4. Rules about a role the user has " "4. Rules about a role the user has "
"(The highest role they have with a rule will be used)\n" "(The highest role they have with a rule will be used).\n"
"5. Rules about the guild a user is in (Owner level only)" "5. Rules about the guild a user is in (Owner level only)."
"\n\nFor more details, please read the official documentation." "\n\nFor more details, please read the official documentation."
) )
@@ -236,7 +231,9 @@ class Permissions:
else: else:
try: try:
testcontext = await self.bot.get_context(message, cls=commands.Context) testcontext = await self.bot.get_context(message, cls=commands.Context)
can = await com.can_run(testcontext) can = await com.can_run(testcontext) and all(
[await p.can_run(testcontext) for p in com.parents]
)
except commands.CheckFailure: except commands.CheckFailure:
can = False can = False
@@ -254,15 +251,16 @@ class Permissions:
Take a YAML file upload to set permissions from Take a YAML file upload to set permissions from
""" """
if not ctx.message.attachments: if not ctx.message.attachments:
return await ctx.send(_("You must upload a file")) return await ctx.send(_("You must upload a file."))
try: try:
await yamlset_acl(ctx, config=self.config.owner_models, update=False) await yamlset_acl(ctx, config=self.config.owner_models, update=False)
except Exception as e: except Exception as e:
print(e) print(e)
return await ctx.send(_("Inalid syntax")) return await ctx.send(_("Invalid syntax."))
else: else:
await ctx.send(_("Rules set.")) await ctx.send(_("Rules set."))
self.invalidate_cache()
@checks.is_owner() @checks.is_owner()
@permissions.command(name="getglobalacl") @permissions.command(name="getglobalacl")
@@ -280,15 +278,16 @@ class Permissions:
Take a YAML file upload to set permissions from Take a YAML file upload to set permissions from
""" """
if not ctx.message.attachments: if not ctx.message.attachments:
return await ctx.send(_("You must upload a file")) return await ctx.send(_("You must upload a file."))
try: try:
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=False) await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=False)
except Exception as e: except Exception as e:
print(e) print(e)
return await ctx.send(_("Inalid syntax")) return await ctx.send(_("Invalid syntax."))
else: else:
await ctx.send(_("Rules set.")) await ctx.send(_("Rules set."))
self.invalidate_cache(ctx.guild.id)
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
@@ -309,15 +308,16 @@ class Permissions:
Use this to not lose existing rules Use this to not lose existing rules
""" """
if not ctx.message.attachments: if not ctx.message.attachments:
return await ctx.send(_("You must upload a file")) return await ctx.send(_("You must upload a file."))
try: try:
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=True) await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=True)
except Exception as e: except Exception as e:
print(e) print(e)
return await ctx.send(_("Inalid syntax")) return await ctx.send(_("Invalid syntax."))
else: else:
await ctx.send(_("Rules set.")) await ctx.send(_("Rules set."))
self.invalidate_cache(ctx.guild.id)
@checks.is_owner() @checks.is_owner()
@permissions.command(name="updateglobalacl") @permissions.command(name="updateglobalacl")
@@ -328,15 +328,16 @@ class Permissions:
Use this to not lose existing rules Use this to not lose existing rules
""" """
if not ctx.message.attachments: if not ctx.message.attachments:
return await ctx.send(_("You must upload a file")) return await ctx.send(_("You must upload a file."))
try: try:
await yamlset_acl(ctx, config=self.config.owner_models, update=True) await yamlset_acl(ctx, config=self.config.owner_models, update=True)
except Exception as e: except Exception as e:
print(e) print(e)
return await ctx.send(_("Inalid syntax")) return await ctx.send(_("Invalid syntax."))
else: else:
await ctx.send(_("Rules set.")) await ctx.send(_("Rules set."))
self.invalidate_cache()
@checks.is_owner() @checks.is_owner()
@permissions.command(name="addglobalrule") @permissions.command(name="addglobalrule")
@@ -348,7 +349,7 @@ class Permissions:
who_or_what: str, who_or_what: str,
): ):
""" """
adds something to the rules Adds something to the rules
allow_or_deny: "allow" or "deny", depending on the rule to modify allow_or_deny: "allow" or "deny", depending on the rule to modify
@@ -363,7 +364,7 @@ class Permissions:
""" """
obj = self.find_object_uniquely(who_or_what) obj = self.find_object_uniquely(who_or_what)
if not obj: if not obj:
return await ctx.send(_("No unique matches. Try using an ID or mention")) return await ctx.send(_("No unique matches. Try using an ID or mention."))
model_type, type_name = cog_or_command model_type, type_name = cog_or_command
async with self.config.owner_models() as models: async with self.config.owner_models() as models:
data = {k: v for k, v in models.items()} data = {k: v for k, v in models.items()}
@@ -380,6 +381,7 @@ class Permissions:
data[model_type][type_name][allow_or_deny].append(obj) data[model_type][type_name][allow_or_deny].append(obj)
models.update(data) models.update(data)
await ctx.send(_("Rule added.")) await ctx.send(_("Rule added."))
self.invalidate_cache(type_name, obj)
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
@@ -392,7 +394,7 @@ class Permissions:
who_or_what: str, who_or_what: str,
): ):
""" """
adds something to the rules Adds something to the rules
allow_or_deny: "allow" or "deny", depending on the rule to modify allow_or_deny: "allow" or "deny", depending on the rule to modify
@@ -407,7 +409,7 @@ class Permissions:
""" """
obj = self.find_object_uniquely(who_or_what) obj = self.find_object_uniquely(who_or_what)
if not obj: if not obj:
return await ctx.send(_("No unique matches. Try using an ID or mention")) return await ctx.send(_("No unique matches. Try using an ID or mention."))
model_type, type_name = cog_or_command model_type, type_name = cog_or_command
async with self.config.guild(ctx.guild).owner_models() as models: async with self.config.guild(ctx.guild).owner_models() as models:
data = {k: v for k, v in models.items()} data = {k: v for k, v in models.items()}
@@ -424,6 +426,7 @@ class Permissions:
data[model_type][type_name][allow_or_deny].append(obj) data[model_type][type_name][allow_or_deny].append(obj)
models.update(data) models.update(data)
await ctx.send(_("Rule added.")) await ctx.send(_("Rule added."))
self.invalidate_cache(type_name, obj)
@checks.is_owner() @checks.is_owner()
@permissions.command(name="removeglobalrule") @permissions.command(name="removeglobalrule")
@@ -450,7 +453,7 @@ class Permissions:
""" """
obj = self.find_object_uniquely(who_or_what) obj = self.find_object_uniquely(who_or_what)
if not obj: if not obj:
return await ctx.send(_("No unique matches. Try using an ID or mention")) return await ctx.send(_("No unique matches. Try using an ID or mention."))
model_type, type_name = cog_or_command model_type, type_name = cog_or_command
async with self.config.owner_models() as models: async with self.config.owner_models() as models:
data = {k: v for k, v in models.items()} data = {k: v for k, v in models.items()}
@@ -467,6 +470,7 @@ class Permissions:
data[model_type][type_name][allow_or_deny].remove(obj) data[model_type][type_name][allow_or_deny].remove(obj)
models.update(data) models.update(data)
await ctx.send(_("Rule removed.")) await ctx.send(_("Rule removed."))
self.invalidate_cache(obj, type_name)
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
@@ -494,7 +498,7 @@ class Permissions:
""" """
obj = self.find_object_uniquely(who_or_what) obj = self.find_object_uniquely(who_or_what)
if not obj: if not obj:
return await ctx.send(_("No unique matches. Try using an ID or mention")) return await ctx.send(_("No unique matches. Try using an ID or mention."))
model_type, type_name = cog_or_command model_type, type_name = cog_or_command
async with self.config.guild(ctx.guild).owner_models() as models: async with self.config.guild(ctx.guild).owner_models() as models:
data = {k: v for k, v in models.items()} data = {k: v for k, v in models.items()}
@@ -511,23 +515,18 @@ class Permissions:
data[model_type][type_name][allow_or_deny].remove(obj) data[model_type][type_name][allow_or_deny].remove(obj)
models.update(data) models.update(data)
await ctx.send(_("Rule removed.")) await ctx.send(_("Rule removed."))
self.invalidate_cache(obj, type_name)
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
@permissions.command(name="setdefaultguildrule") @permissions.command(name="setdefaultguildrule")
async def set_default_guild_rule( async def set_default_guild_rule(
self, ctx: commands.Context, cog_or_command: CogOrCommand, allow_or_deny: RuleType = None self, ctx: commands.Context, allow_or_deny: ClearableRuleType, cog_or_command: CogOrCommand
): ):
""" """
Sets the default behavior for a cog or command if no rule is set Sets the default behavior for a cog or command if no rule is set
Use with a cog or command and no setting to clear the default and defer to
normal check logic
""" """
if allow_or_deny: val_to_set = {"allow": True, "deny": False, "clear": None}.get(allow_or_deny)
val_to_set = {"allow": True, "deny": False}.get(allow_or_deny)
else:
val_to_set = None
model_type, type_name = cog_or_command model_type, type_name = cog_or_command
async with self.config.guild(ctx.guild).owner_models() as models: async with self.config.guild(ctx.guild).owner_models() as models:
@@ -540,24 +539,18 @@ class Permissions:
data[model_type][type_name]["default"] = val_to_set data[model_type][type_name]["default"] = val_to_set
models.update(data) models.update(data)
await ctx.send(_("Defualt set.")) await ctx.send(_("Default set."))
self.invalidate_cache(type_name)
@checks.is_owner() @checks.is_owner()
@permissions.command(name="setdefaultglobalrule") @permissions.command(name="setdefaultglobalrule")
async def set_default_global_rule( async def set_default_global_rule(
self, ctx: commands.Context, cog_or_command: CogOrCommand, allow_or_deny: RuleType = None self, ctx: commands.Context, allow_or_deny: ClearableRuleType, cog_or_command: CogOrCommand
): ):
""" """
Sets the default behavior for a cog or command if no rule is set Sets the default behavior for a cog or command if no rule is set
Use with a cog or command and no setting to clear the default and defer to
normal check logic
""" """
val_to_set = {"allow": True, "deny": False, "clear": None}.get(allow_or_deny)
if allow_or_deny:
val_to_set = {"allow": True, "deny": False}.get(allow_or_deny)
else:
val_to_set = None
model_type, type_name = cog_or_command model_type, type_name = cog_or_command
async with self.config.owner_models() as models: async with self.config.owner_models() as models:
@@ -570,33 +563,18 @@ class Permissions:
data[model_type][type_name]["default"] = val_to_set data[model_type][type_name]["default"] = val_to_set
models.update(data) models.update(data)
await ctx.send(_("Defualt set.")) await ctx.send(_("Default set."))
self.invalidate_cache(type_name)
@commands.bot_has_permissions(add_reactions=True)
@checks.is_owner() @checks.is_owner()
@permissions.command(name="clearglobalsettings") @permissions.command(name="clearglobalsettings")
async def clear_globals(self, ctx: commands.Context): async def clear_globals(self, ctx: commands.Context):
""" """
Clears all global rules. Clears all global rules.
""" """
await self._confirm_then_clear_rules(ctx, is_guild=False)
self.invalidate_cache()
m = await ctx.send("Are you sure?")
for r in REACTS.keys():
await m.add_reaction(r)
try:
reaction, user = await self.bot.wait_for(
"reaction_add", check=lambda r, u: u == ctx.author and str(r) in REACTS, timeout=30
)
except asyncio.TimeoutError:
return await ctx.send(_("Ok, try responding with an emoji next time."))
if REACTS.get(str(reaction)):
await self.config.owner_models.clear()
await ctx.send(_("Global settings cleared"))
else:
await ctx.send(_("Okay."))
@commands.bot_has_permissions(add_reactions=True)
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
@permissions.command(name="clearguildsettings") @permissions.command(name="clearguildsettings")
@@ -604,23 +582,61 @@ class Permissions:
""" """
Clears all guild rules. Clears all guild rules.
""" """
await self._confirm_then_clear_rules(ctx, is_guild=True)
self.invalidate_cache(ctx.guild.id)
m = await ctx.send("Are you sure?") async def _confirm_then_clear_rules(self, ctx: commands.Context, is_guild: bool):
if ctx.guild.me.permissions_in(ctx.channel).add_reactions:
m = await ctx.send(_("Are you sure?"))
for r in REACTS.keys(): for r in REACTS.keys():
await m.add_reaction(r) await m.add_reaction(r)
try: try:
reaction, user = await self.bot.wait_for( reaction, user = await self.bot.wait_for(
"reaction_add", check=lambda r, u: u == ctx.author and str(r) in REACTS, timeout=30 "reaction_add",
check=lambda r, u: u == ctx.author and str(r) in REACTS,
timeout=30,
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:
return await ctx.send(_("Ok, try responding with an emoji next time.")) return await ctx.send(_("Ok, try responding with an emoji next time."))
if REACTS.get(str(reaction)): agreed = REACTS.get(str(reaction))
else:
await ctx.send(_("Are you sure? (y/n)"))
try:
message = await self.bot.wait_for(
"message",
check=lambda m: m.author == ctx.author and m.content in Y_OR_N,
timeout=30,
)
except asyncio.TimeoutError:
return await ctx.send(_("Ok, try responding with yes or no next time."))
agreed = Y_OR_N.get(message.content.lower())
if agreed:
if is_guild:
await self.config.guild(ctx.guild).owner_models.clear() await self.config.guild(ctx.guild).owner_models.clear()
await ctx.send(_("Guild settings cleared")) await ctx.send(_("Guild settings cleared."))
else:
await self.config.owner_models.clear()
await ctx.send(_("Global settings cleared."))
else: else:
await ctx.send(_("Okay.")) await ctx.send(_("Okay."))
def invalidate_cache(self, *to_invalidate):
"""
Either invalidates the entire cache (if given no objects)
or does a partial invalidation based on passed objects
"""
if len(to_invalidate) == 0:
self.cache.clear()
return
# LRUDict inherits from ordered dict, hence the syntax below
stil_valid = [
(k, v) for k, v in self.cache.items() if not any(obj in k for obj in to_invalidate)
]
self.cache = LRUDict(stil_valid, size=self.cache.size)
def find_object_uniquely(self, info: str) -> int: def find_object_uniquely(self, info: str) -> int:
""" """
Finds an object uniquely, returns it's id or returns None Finds an object uniquely, returns it's id or returns None

View File

@@ -7,26 +7,32 @@ from redbot.core import commands
log = logging.getLogger("redbot.cogs.permissions.resolvers") 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: 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 Returns the value from a check if it is valid
""" """
# Non staticmethods should not be run without their parent
# class, even if the parent class did not deregister them
if check.__module__ is None:
pass
elif isinstance(check, types.FunctionType):
if (
next(filter(lambda x: check.__module__ == x.__module__, ctx.bot.cogs.values()), None)
is None
):
return None
val = None val = None
# let's not spam the console with improperly made 3rd party checks # let's not spam the console with improperly made 3rd party checks
try: try:
if asyncio.iscoroutine(check) or asyncio.iscoroutinefunction(check): if asyncio.iscoroutinefunction(check):
val = await check(ctx, level=level) val = await check(ctx, level=level)
else: else:
val = check(ctx, level=level) val = check(ctx, level=level)
@@ -67,23 +73,7 @@ def resolve_lists(*, ctx: commands.Context, whitelist: list, blacklist: list) ->
""" """
resolves specific lists resolves specific lists
""" """
for entry in entries_from_ctx(ctx):
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
for entry in entries:
if entry in whitelist: if entry in whitelist:
return True return True
if entry in blacklist: if entry in blacklist:

View File

@@ -59,29 +59,29 @@ class Reports:
@commands.group(name="reportset") @commands.group(name="reportset")
async def reportset(self, ctx: commands.Context): async def reportset(self, ctx: commands.Context):
""" """
settings for 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 setoutput(self, ctx: commands.Context, channel: discord.TextChannel):
"""sets the output channel""" """Set the channel where reports will show up"""
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(_("Report Channel 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="toggleactive") @reportset.command(name="toggle", aliases=["toggleactive"])
async def report_toggle(self, ctx: commands.Context): async def report_toggle(self, ctx: commands.Context):
"""Toggles whether the Reporting tool is enabled or not""" """Enables or Disables reporting for the 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)
if active: if active:
await ctx.send(_("Reporting now enabled")) await ctx.send(_("Reporting is now enabled"))
else: else:
await ctx.send(_("Reporting disabled.")) await ctx.send(_("Reporting is now disabled."))
async def internal_filter(self, m: discord.Member, mod=False, perms=None): async def internal_filter(self, m: discord.Member, mod=False, perms=None):
ret = False ret = False
@@ -105,7 +105,7 @@ class Reports:
*, *,
mod: bool = False, mod: bool = False,
permissions: Union[discord.Permissions, dict] = None, permissions: Union[discord.Permissions, dict] = None,
prompt: str = "" prompt: str = "",
): ):
""" """
discovers which of shared guilds between the bot discovers which of shared guilds between the bot
@@ -175,7 +175,10 @@ class Reports:
if await self.bot.embed_requested(channel, author): if await self.bot.embed_requested(channel, author):
em = discord.Embed(description=report) em = discord.Embed(description=report)
em.set_author( em.set_author(
name=_("Report from {0.display_name}").format(author), icon_url=author.avatar_url name=_("Report from {author}{maybe_nick}").format(
author=author, maybe_nick=(f" ({author.nick})" if author.nick else "")
),
icon_url=author.avatar_url,
) )
em.set_footer(text=_("Report #{}").format(ticket_number)) em.set_footer(text=_("Report #{}").format(ticket_number))
send_content = None send_content = None
@@ -201,10 +204,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 = ""):
""" """
Follow the prompts to make a report Send a report.
optionally use with a report message Use without arguments for interactive reporting, or do
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
@@ -212,11 +215,6 @@ class Reports:
guild = await self.discover_guild( guild = await self.discover_guild(
author, prompt=_("Select a server to make a report in by number.") author, prompt=_("Select a server to make a report in by number.")
) )
else:
try:
await ctx.message.delete()
except discord.Forbidden:
pass
if guild is None: if guild is None:
return return
g_active = await self.config.guild(guild).active() g_active = await self.config.guild(guild).active()
@@ -229,22 +227,18 @@ class Reports:
if self.antispam[guild.id][author.id].spammy: if self.antispam[guild.id][author.id].spammy:
return await author.send( return await author.send(
_( _(
"You've sent a few too many of these recently. " "You've sent too many reports recently. "
"Contact a server admin to resolve this, or try again " "Please contact a server admin if this is important matter, "
"later." "or please wait and try again later."
) )
) )
if author.id in self.user_cache: if author.id in self.user_cache:
return await author.send( return await author.send(
_("Finish making your prior report " "before making an additional one") _(
"Please finish making your prior report before trying to make an "
"additional one!"
)
) )
if ctx.guild:
try:
await ctx.message.delete()
except (discord.Forbidden, discord.HTTPException):
pass
self.user_cache.append(author.id) self.user_cache.append(author.id)
if _report: if _report:
@@ -261,9 +255,7 @@ class Reports:
) )
) )
except discord.Forbidden: except discord.Forbidden:
await ctx.send(_("This requires DMs enabled.")) return await ctx.send(_("This requires DMs enabled."))
self.user_cache.remove(author.id)
return
def pred(m): def pred(m):
return m.author == author and m.channel == dm.channel return m.author == author and m.channel == dm.channel
@@ -271,18 +263,32 @@ class Reports:
try: try:
message = await self.bot.wait_for("message", check=pred, timeout=180) message = await self.bot.wait_for("message", check=pred, timeout=180)
except asyncio.TimeoutError: except asyncio.TimeoutError:
await author.send(_("You took too long. Try again later.")) return await author.send(_("You took too long. Try again later."))
else: else:
val = await self.send_report(message, guild) val = await self.send_report(message, guild)
with contextlib.suppress(discord.Forbidden, discord.HTTPException): with contextlib.suppress(discord.Forbidden, discord.HTTPException):
if val is None: if val is None:
await author.send(_("There was an error sending your report.")) await author.send(
_("There was an error sending your report, please contact a server admin.")
)
else: else:
await author.send(_("Your report was submitted. (Ticket #{})").format(val)) await author.send(_("Your report was submitted. (Ticket #{})").format(val))
self.antispam[guild.id][author.id].stamp() self.antispam[guild.id][author.id].stamp()
self.user_cache.remove(author.id) @report.after_invoke
async def report_cleanup(self, ctx: commands.Context):
"""
The logic is cleaner this way
"""
if ctx.author.id in self.user_cache:
self.user_cache.remove(ctx.author.id)
if ctx.guild and ctx.invoked_subcommand is None:
if ctx.channel.permissions_for(ctx.guild.me).manage_messages:
try:
await ctx.message.delete()
except discord.NotFound:
pass
async def on_raw_reaction_add(self, payload): async def on_raw_reaction_add(self, payload):
""" """
@@ -311,17 +317,19 @@ class Reports:
if msgs: if msgs:
self.tunnel_store[k]["msgs"] = msgs self.tunnel_store[k]["msgs"] = msgs
@commands.guild_only()
@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):
""" """
opens a message tunnel between things you say in this channel Open a message tunnel.
and the ticket opener's direct messages
tunnels do not persist across bot restarts This tunnel will forward things you say in this channel
to the ticket opener's direct messages.
Tunnels do not persist across bot restarts.
""" """
# note, mod_or_permissions is an implicit guild_only
guild = ctx.guild guild = ctx.guild
rec = await self.config.custom("REPORT", guild.id, ticket_number).report() rec = await self.config.custom("REPORT", guild.id, ticket_number).report()
@@ -344,14 +352,15 @@ class Reports:
) )
big_topic = _( big_topic = _(
"{who} opened a 2-way communication." "{who} opened a 2-way communication "
"about ticket number {ticketnum}. 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 " "You can close a communication at any point by reacting with "
"by reacting with the X to the last message recieved. " "the \N{NEGATIVE SQUARED CROSS MARK} to the last message recieved.\n"
"\nAny message succesfully forwarded will be marked with a check." "Any message succesfully forwarded will be marked with "
"\nTunnels are not persistent across bot restarts." "\N{WHITE HEAVY CHECK MARK}.\n"
"Tunnels are not persistent across bot restarts."
) )
topic = big_topic.format( topic = big_topic.format(
ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild) ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild)
@@ -359,8 +368,7 @@ class Reports:
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)
except discord.Forbidden: except discord.Forbidden:
await ctx.send(_("User has disabled DMs.")) await ctx.send(_("That user has DMs disabled."))
tun.close()
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(big_topic.format(who=_("You have"), ticketnum=ticket_number))

View File

@@ -1,5 +1,7 @@
from .streams import Streams from .streams import Streams
def setup(bot): async def setup(bot):
bot.add_cog(Streams(bot)) cog = Streams(bot)
await cog.initialize()
bot.add_cog(cog)

View File

@@ -1,17 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-18 14:42+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"

View File

@@ -1,11 +0,0 @@
import subprocess
TO_TRANSLATE = ["../mod.py"]
def regen_messages():
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":
regen_messages()

Some files were not shown because too many files have changed in this diff Show More