Compare commits

..

153 Commits

Author SHA1 Message Date
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
palmtree5
e34975001c [V3] Bump version to 3.0.0b15 (#1720) 2018-05-27 21:48:05 -08:00
Michael H
f3b282062b get channels (#1729) 2018-05-27 21:42:47 -08:00
Michael H
84732a24fa [V3] Drop verbose output on check failure (#1725) 2018-05-27 21:27:44 -08:00
Michael H
dad775b494 [V3 Context] use bot's color if it has one (Ux Consistency with help formatter) (#1706)
* use bot's color if it has one

* add bot color support to context

* alias color to colour too to match d.py consistency

* Update context.py

* Update context.py

* black fix
2018-05-27 21:23:03 -08:00
palmtree5
05ad3fcd5c [V3 Modlog] add events for modlog cases (#1717)
* Give modlog case objects the bot as an attribute

* Dispatch modlog_case_create and modlog_case_edit events

* case.bot, not just bot

* fix a couple more issues resulting from refactor

* Case.edit doesn't need the bot parameter lol

* Make create_case return the case object (because tests)

* Modify create_case docstring

* Fix a docstring
2018-05-27 21:18:50 -08:00
Redjumpman
6ae02d2d02 [V3/Readme] Update V3 Readme (#1703)
* Update and rename README.rst to README.MD

* Update and rename README.MD to README.rst

Changed the file type back to rst from MD

* Update README.rst

Changed image size.

* Update README.rst

Changed the cogs.red link to point at issue 1398 until the portal displays V3 cogs.

* Update README.rst

Changed image host to imgur
2018-05-27 21:14:24 -08:00
Michael H
757a3114dc [V3] rpc close removed (#1726) 2018-05-27 21:06:13 -08:00
palmtree5
94b9878c6c [V3 Downloader] add repo info command + add short descriptions in repo list (#1701) 2018-05-28 07:01:54 +02:00
palmtree5
7775b16199 [V3] Optimize the backup command (#1666)
* [V3 Core] Enhance [p]backup to exclude some files

* Backup the repo list too

* Lol Sinbad's pre-commit hook

* Add option of sending the backup to the owner via DM

* Drop an unnecessary config object in RepoManager

* Move the backup functionality in redbot-setup to the new stuff

* More work on implementation, including backing up the instance data
2018-05-28 06:56:28 +02:00
palmtree5
f01d48f9ae [V3 Docs] allow [p]shutdown to actually shut the bot down (#1668) 2018-05-28 06:51:31 +02:00
palmtree5
179883094e [V3 Context] make send_help respect embed setting (#1723) 2018-05-28 06:37:58 +02:00
palmtree5
971ccf9df4 [V3 Core] add support for setting a color for embeds (#1707)
* [V3 Core] add support for setting a color for embeds

* Add a guild toggle for whether to use the bot color

* Add a function for getting embed color in Context

* Coroutines need to be awaited lol
2018-05-28 06:28:22 +02:00
Michael H
07eb6bf88e Reverted Ping back to its original state (#1712) 2018-05-28 06:17:02 +02:00
palmtree5
5afd8174ca [V3 Launcher] update cli flag getter (#1696)
* [V3 Launcher] make some updates to the cli flag selector

* Add the --mentionable flag
2018-05-28 06:06:59 +02:00
aikaterna
f1fea38712 [V3 Mod] Unmute server does not need channel (#1695) 2018-05-28 05:36:44 +02:00
El Laggron
f275c6e5e7 [V3 Launcher] Fixed issue with update choice (#1649)
*  [V3 Launcher] Fixed issue with update choice

extras_selectors() was run even if what the user did input for the development choice (stable/dev) was wrong

* [V3 Launcher] Option to go back when updating

* [V3 Launcher] Fixed coding style
2018-05-28 05:30:35 +02:00
palmtree5
5ec25959df [V3 Help] add tagline support (#1705)
* [V3 Help] add tagline support

* Make the tagline resettable

* Actually, let's allow the user full control over the footer
2018-05-28 05:25:18 +02:00
palmtree5
4f270f3aab [V3] Start work on fuzzy command search (#1600)
* [V3] Start work on fuzzy command search

* Implement in command error handler

* Something isn't working here, try fixing

* Style compliance

* Add fuzzywuzzy to pipfile

* Dump the short doc part if there is no short doc

* Add fuzzy command search on command not found in help

* Move things around, implement for use of default d.py help formatter

* Formatting compliance

* Undo pipfile changes
2018-05-28 04:57:10 +02:00
bobloy
4028dd3009 [V3 Downloader] Pagify cog list and typing fix (#1662)
* Cog list is now pagified

* Proper typing of Tuple

* Black formatting

* More Black formatting
2018-05-28 03:55:33 +02:00
Tobotimus
706b04610d [V3] Implement --dry-run flag (#1648) 2018-05-28 03:46:06 +02:00
Michael H
014e3baea0 pagification (#1722) 2018-05-28 03:35:57 +02:00
Michael H
92ca7c935a Docstring fix (#1724) 2018-05-28 03:30:58 +02:00
palmtree5
5c9b1c9a3d Move [p]userinfo to Mod + refactor [p]names (#1719) 2018-05-28 03:23:24 +02:00
Tobotimus
5ebee60c97 Rejoice for a 3.5-less Travis (#1713) 2018-05-28 03:18:23 +02:00
Tobotimus
3337a9cbab [V3 Launcher] Fix error when removing Mongo instance (#1710)
* [V3 Launcher] Fix error when removing Mongo instance

Fixes #1573

* Fix issue causing style check to fail

* Remove unneeded whitespace
2018-05-27 16:08:25 -08:00
Michael H
54975eb812 [V3] Permissions (#1548)
* This starts setting up checks.py to handle managed permission overrides

* A decent starting point, more work to come

* missing else fix

* more work on this

* reduce redundant code

* More work on this...

* more progress

* add a debug flag to some things in .resolvers to help with exploring why checks behave in a certain way

* modify this to be a list for ease of showing full resolution order

* more

* don't bypass is_owner, ever

* remove old logic about ownercommands

* better handling of chec validity

* anonymous functions return None for __module__, remove some code as a result

* mutable default bind fix

* Add a caching layer (to be invalidated as needed)

Ensure checks in the chain inserted before the core logic only return None or False
(whitelists then blacklists are checked first in core logic, from most to least specific scope, overriding this with an allow does not make sense)

* more progress, slow work as I have time

* Modifies the predicates so that their inner functions are accesible from cogs without
being a check

* Update checks.py

Safety for existing permissions.py cogs

* This is where I take a change of course on setting this up,
because this would have been the most long winded interactive command ever as
it was starting to progress.

This is going to support individual entry updates, settings from yaml, gettings, and clearing existing settings
as well as printing a settings template out and referring people to what is going to be very well written docs

* block permissions cog from being unblocked by the permissions cog as a safety feature (really, co-owner exists at this point)

* WIP

* Okay, this has the intent of the changes, just to actually test these as working as intended + add corresponding guild functions

* oh nice, missed a couple files, sec...

* WIP, also, something's broken in resolvers or check_overrides >>

* This is working now (still needs docs and more...)

* unmerge changes from other PR

* is_owner still needs to exist in here due to management of non checked commands

* Update this to new style standards

* forgot to commit some local changes earlier

* fix update logic

* fix update logic

* b14 fix, lol

* fix issue with management command name

* this isnt a real fix

* Ok..

* perms

* This is working, but needs docs and more configuration opts now

* more

* Ux functions, need testing

* style

* fix using the obj str rather than the id

* fix CogOrCommand converter

* Return the correct things in the converter

* last fix, needs docs, and possibly some extra Ux utils

* start doc writing

* extra user facing commands

* yaml docs

* yaml fix

* secondary checks-fix

* 3rd party check stuff

* remove warning that this isn't ready yet

* swap ctx.tick for real responses, require emoji perms for interactive menuing, better attr handling for nicknames

* send file to author

* alias to `p`

* more ctx tick removal

(This is a long ass changelog...)
2018-05-28 00:17:17 +02:00
rngesus-wept
537531803a [V3 Admin] Correct spelling of 'hierarchy' (#1714) 2018-05-27 12:25:26 -08:00
Tobotimus
f4b640126b [V3 Warnings] Fix warn command when no valid reason is passed (#1672)
Resolves #1670
2018-05-27 12:15:56 -08:00
El Laggron
1de3251127 [V3 Docs] Reference 3.6 docs (#1715) 2018-05-27 12:05:35 -08:00
palmtree5
7e98076e4a [V3 Docs] expand info.json docs (#1699) 2018-05-25 12:38:32 +02:00
palmtree5
c58c55b752 [V3 Docs] Move the install docs to install Python 3.6 (#1685)
* [V3 Docs] drop ffmpeg from CentOS install guide

* [V3 Install Docs] move all to Python 3.6

* Update the toctree

* Needed a blank line

* drop a .6 that wasn't needed
2018-05-24 11:09:08 -08:00
Michael H
928be5717f [V3 Checks] Respect administrator and guildowner permissions (#1711)
* respect admin and guildowner (implicitly) in checks for permissions

* this needed it too
2018-05-25 01:17:21 +10:00
Redjumpman
ccbaa926ce [V3 Economy] fix erroneous message when transferring with insufficient funds (#1698)
Fixed an erroneous message when transferring credits while having insufficient funds.
2018-05-23 11:45:23 -08:00
Tobotimus
d1208d7d19 [V3] Update CONTRIBUTING.md with details on new dev workflow (#1659)
* Update CONTRIBUTING.md with details on new dev workflow

* Fix typos

* Update link in README.rst

* tiny grammar fix

* Specify the line length

* Update CONTRIBUTING.md

* Fix links in contents

* Add section for keeping dependencies up-to-date

* Include notes about Makefile
2018-05-23 15:26:54 +10:00
Tobotimus
099fe59a97 [V3 Core] Add [p]helpset (#1694)
* Add settings and commands for page and character limits

* Add missing returns

* Consistent responses
2018-05-22 21:04:53 -08:00
Will
889acaec82 [V3 Downloader] Fix #1671 (#1692) 2018-05-22 20:54:00 -04:00
palmtree5
c42e9d4c5c Add a makefile for helping with style checking and reformatting (#1665)
* Add a makefile

* Add make.bat

* Slightly modify Palm's makefile

* Use make in tox

* Minimise diff and refactor PATHEXT

* Fix a typo in make.bat
2018-05-22 20:44:11 -04:00
Will
4378e5295d [V3 Downloader] Fix #1594 (#1693) 2018-05-22 20:37:34 -04:00
Will
73a427f6aa [V3 RPC] Initial RPC library switch (#1634)
* Initial RPC library switch

* Use weak refs to the methods so cog unload works

* Add docs

* Black fixes

* Add jsonrpcserver to Pipfile.lock
2018-05-22 16:29:26 -08:00
aikaterna
abfee70eb3 [V3 Audio] Only allow audio commands in servers (#1682)
* [V3 Audio] Only allow audio commands in servers

Fixes #1681

* [V3 Audio] Formatting fix
2018-05-22 19:39:01 -04:00
Michael H
77cdbf8dd6 [V3 Alias] Add checks to alias add/del (#1676) 2018-05-22 19:27:11 -04:00
aikaterna
28bc68c916 [V3 Audio] [p]llsetup fixes (#1656)
* [V3 Audio] [p]llsetup fixes

[p]llset wsport changed to not use the rest port setting and both the rest and ws ports were changed to use ints instead of strings.

* [V3 Audio] Version change
2018-05-22 19:19:47 -04:00
Will
ecb64cc2ec [V3] Add deprecation warning for Python 3.5 (#1684)
* Add deprecation warning for python 3.5

* Use colorama

* Modify background color

* Just print to stdout
2018-05-20 20:56:50 -08:00
bobloy
23706a1ba9 [V3 Tests] Fix downloader test failing on Windows (#1673)
* ignore idea

* windows option

* Remove personal setting

* proper undo

* Requested changes

```
("repos", "squid") == pathlib.Path('TEST\\\\TEST2\\\\repos\\\\squid').parts[-2:]
True
("repos", "squid") == pathlib.Path('test/test2/test3/test4/repos/squid').parts[-2:]
True
```

* Needs to remove newline too

Resolves #1663
2018-05-19 12:11:41 +10:00
Michael H
d3f406a34a [V3 Travis] Update travis to not skip pipfile lock... (#1678)
* Update travis to not sip pipfile lock

update pipfile dependencies

additional black formatting pass to conform to black 18.5b

* .

* pin async timeout until further discussion of 3.5 support

* .
2018-05-18 17:48:22 -08:00
bobloy
55afc7eb33 [V3 Downloader] Handle errors when importing modules (#1655)
* Handle errors when importing modules

* Do nothing with error

* Updated to black formatter standards

* More Black formatting
2018-05-17 13:42:41 -04:00
Tobotimus
7a70d12efd [V3] Add tox (#1641)
* Configure tox environments for install, dev install and docs build

* Configure Travis to run tox

* Use 3.5.1 since it's our minimum supported version

* Bump lower travis build version to 3.5.2

Turns out a dependency is incompatible with 3.5.1.

* Modify Travis config to install from pipenv

* Try without skipping the lock

* Try without pip cache

* D the dev install with pipenv

* See if adding the pip cache back in breaks

* Remove the development installation

It doesn't really make any sense considering we already should be installed in develop mode, as does Travis.

* Oops, tox should go under dev packages

* Do black --check with tox

* Uncache pip again...

* Try a build matrix, and try ignoring virtualenvs

* Activate pipenv shell on travis

* Try installing prereleases

* Try the build matrix like this

* Try exclusion

* Upgrade pip

* Try this environment marker

* Back to stages...

* Try run over shell

* Try skipping the lock again

* This'll be faster but probably ignore 3.5

Because Travis

* Just manually list sources for black to check

* Magic?

* What if I told you...

That this worked perfectly on Tobotimus/Red-DiscordBot@test_travis_matrix

* It couldn't possibly be this easy

* Let's add some comments to be nice

* Let's change back to trusty just in case the stages fuck up

* Add another comment because why not

* Let's try caching pip one more time

* We don't need to whitelist these
2018-05-15 13:10:14 +10:00
palmtree5
1ecaf6f8d5 Black formatting for generate_strings.py and docs/conf.py (#1658)
* Black formatting for generate_strings.py

* Also add docs/conf.py
2018-05-15 09:27:06 +10:00
Will
e01cdbb091 Black tests and setup.py (#1657) 2018-05-15 09:09:54 +10:00
Michael H
b88b5a2601 [V3] Update code standards (black code format pass) (#1650)
* ran black: code formatter against `redbot/` with `-l 99`

* badge
2018-05-14 15:33:24 -04:00
Michael H
e7476edd68 [V3] fix help on missing cog and command docstrings (#1645)
* [V3] fix help on missing cog docstrings

* I think this is what you're looking for

* Also do a NoneType check for commands
2018-05-14 20:13:12 +10:00
Will
cbbeb412f9 [V3 Docs] Fix makefile and add dpy back in to the requirements (#1646) 2018-05-14 15:30:42 +10:00
Will
f544890f00 [V3 Docs] Modify RTD config to (hopefully) make it build (#1644)
* Fix docs requirements

* Modify RTD config
2018-05-14 15:03:43 +10:00
Will
72560fa6d0 [V3] Add pipenv files (#1642)
* Add pipenv files

* Pipfile updates

* Update sphinx version
2018-05-14 14:20:20 +10:00
Tobotimus
4637ff78c0 [V3 Docs] Remove all build warnings (#1640)
* Upgrade sphinx version to 1.7+

* Fix title overlines/underlines in autostart_systemd.rst

* Skip trying to document a method from discord.py

* Add escaped space after backtick

* Escape underscores (sphinx tries to interpret a hyperlink)

* Use fully qualified reference for class

* Fix reference in tunnel.py

* Remove python syntax highlighting in data_converter.py

For some reason sphinx couldn't lex these as python. Removing the highlighting seems like the logical solution for now, since if it wasn't being lexed, it wouldn't highlight anyway.

* Comment out static path since we're not using it right now

* Update sphinx version in docs requirements too

Would rather remove this duplication but RTD is a special snowflake
2018-05-13 20:06:52 -08:00
161 changed files with 8387 additions and 4568 deletions

3
.github/CODEOWNERS vendored
View File

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

View File

@@ -1,23 +1,43 @@
# Introduction
### Welcome!
First off, thank you for contributing to the further development of Red. We're always looking for new ways to improve our project and we appreciate any help you can give us.
# Contents
* [1. Introduction](#1-introduction)
* [1.1 Why do these guidelines exist?](#11-why-do-these-guidelines-exist)
* [1.2 What kinds of contributions are we looking for?](#12-what-kinds-of-contributions-are-we-looking-for)
* [2. Ground Rules](#2-ground-rules)
* [3. Your First Contribution](#3-your-first-contribution)
* [4. Getting Started](#4-getting-started)
* [4.1 Setting up your development environment](#41-setting-up-your-development-environment)
* [4.2 Testing](#42-testing)
* [4.3 Style](#43-style)
* [4.4 Make](#44-make)
* [4.5 Keeping your dependencies up to date](#45-keeping-your-dependencies-up-to-date)
* [4.6 To contribute changes](#46-to-contribute-changes)
* [4.7 How To Report A Bug](#47-how-to-report-a-bug)
* [4.8 How To Suggest A Feature Or Enhancement](#48-how-to-suggest-a-feature-or-enhancement)
* [5. Code Review Process](#5-code-review-process)
* [5.1 Issues](#51-issues)
* [5.2 Pull Requests](#52-pull-requests)
* [5.3 Differences between "new features" and "improvements"](#53-differences-between-new-features-and-improvements)
* [6. Community](#6-community)
### Why do these guidelines exist?
# 1. Introduction
**Welcome!** First off, thank you for contributing to the further development of Red. We're always looking for new ways to improve our project and we appreciate any help you can give us.
### 1.1 Why do these guidelines exist?
Red is an open source project. This means that each and every one of the developers and contributors who have helped make Red what it is today have done so by volunteering their time and effort. It takes a lot of time to coordinate and organize issues and new features and to review and test pull requests. By following these guidelines you will help the developers streamline the contribution process and save them time. In doing so we hope to get back to each and every issue and pull request in a timely manner.
### What kinds of contributions are we looking for?
### 1.2 What kinds of contributions are we looking for?
We love receiving contributions from our community. Any assistance you can provide with regards to bug fixes, feature enhancements, and documentation is more than welcome.
# 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.
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:
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.
6. Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. See [Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/).
# Your First Contribution
# 3. Your First Contribution
Unsure of how to get started contributing to Red? Please take a look at the Issues section of this repo and sort by the following labels:
* beginner - issues that can normally be fixed in just a few lines of code and maybe a test or two.
@@ -27,35 +47,87 @@ Unsure of how to get started contributing to Red? Please take a look at the Issu
At this point you're ready to start making changes. Feel free to ask for help; everyone was a beginner at some point!
# Getting Started
### Testing
We've recently started adding unit-testing into Red. All current tests can be found in the `tests/` directory at the root level of the repository. You will need `py.test` installed in order to run them (which is already in `requirement.txt`). Tests can be run by simply calling `pytest` once you've `cd`'d into the Red repository folder.
# 4. Getting Started
### To contribute changes
1. Create your own fork of the Red repository.
2. Make the changes in your own fork.
Red's repository is configured to follow a particular development workflow, using various reputable tools. We kindly ask that you stick to this workflow when contributing to Red, by following the guides below. This will help you to easily produce quality code, identify errors early, and streamline the code review process.
### 4.1 Setting up your development environment
The following requirements must be installed prior to setting up:
- Python 3.6
- git
- pip
- pipenv
If you're not on Windows, you can optionally install [pyenv](https://github.com/pyenv/pyenv), which will help you run tests for different python versions.
1. Fork and clone the repository to a directory on your local machine.
2. Open a command line in that directory and execute the following commands:
```bash
pip install pipenv
pipenv install --dev
```
Red, its dependencies, and all required development tools, are now installed to a virtual environment. Red is installed in editable mode, meaning that edits you make to the source code in the repository will be reflected when you run Red.
3. Activate the new virtual environment with the command:
```bash
pipenv shell
```
From here onwards, we will assume you are executing commands from within this shell. Each time you open a new command line, you should execute this command first.
Note: If you haven't used `pipenv` before but are comfortable with virtualenvs, just run `pip install pipenv` in the virtualenv you're already using and invoke the command above from the cloned Red repo. It will do the correct thing.
### 4.2 Testing
We've recently started using [tox](https://github.com/tox-dev/tox) to run all of our tests. It's extremely simple to use, and if you followed the previous section correctly, it is already installed to your virtual environment.
Currently, tox does the following, creating its own virtual environments for each stage:
- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on python 3.6 (test environment `py36`)
- 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`)
To run all of these tests, just run the command `tox` in the project directory.
To run a subset of these tests, use the command `tox -e <env>`, where `<env>` is the test environment you want tox to run. The test environments are noted in the dot points above.
Your PR will not be merged until all of these tests pass.
### 4.3 Style
Our style checker of choice, [black](https://github.com/ambv/black), actually happens to be an auto-formatter. The checking functionality simply detects whether or not it would try to reformat something in your code, should you run the formatter on it. For this reason, we recommend using this tool as a formatter, regardless of any disagreements you might have with the style it enforces.
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>`.
### 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:
1. `make reformat`: Reformat all python files in the project with Black
2. `make stylecheck`: Check if any `.py` files in the project need reformatting
### 4.5 Keeping your dependencies up to date
Whenever you pull from upstream (V3/develop on the main repository) and you notice the file `Pipfile.lock` has been changed, it usually means one of the package dependencies have been updated, added or removed. To make sure you're testing and formatting with the most up-to-date versions of our dependencies, run `pipenv install --dev` again.
### 4.6 To contribute changes
1. Create a new branch on your fork
2. Make the changes
3. If you like the changes and think the main Red project could use it:
* Ensure your code follows (generally) the PEP8 Python style guide
* Run tests with `tox` to ensure your code is up to scratch
* Create a Pull Request on GitHub with your changes
### How To Report A Bug
### 4.7 How To Report A Bug
Please see our **ISSUES.MD** for more information.
### How To Suggest A Feature Or Enhancement
### 4.8 How To Suggest A Feature Or Enhancement
The goal of Red is to be as useful to as many people as possible, this means that all features must be useful to anyone and any server that uses Red.
If you find yourself wanting a feature that Red does not already have, you're probably not alone. There's bound to be a great number of users out there needing the same thing and a lot of the features that Red has today have been added because of the needs of our users. Open an issue on our issues list and describe the feature you would like to see, how you would use it, how it should work, and why it would be useful to the Red community as a whole.
# Code Review Process
# 5. Code Review Process
We have a core team working tirelessly to implement new features and fix bugs for the Red community. This core team looks at and evaluates new issues and PRs on a daily basis.
The decisions we make are based on a simple majority of that team or by decree of the project owner.
### Issues
### 5.1 Issues
Any new issues will be looked at and evaluated for validity of a bug or for the usefulness of a suggested feature. If we have questions about your issue we will get back as soon as we can (usually in a day or two) and will try to make a decision within a week.
### Pull Requests
### 5.2 Pull Requests
Pull requests are evaluated by their quality and how effectively they solve their corresponding issue. The process for reviewing pull requests is as follows:
1. A pull request is submitted
@@ -66,10 +138,10 @@ Pull requests are evaluated by their quality and how effectively they solve thei
4. If any feedback is given we expect a response within 1 week or we may decide to close the PR.
5. If your pull request is not vetoed and no core member requests changes then it will be approved and merged into the project.
### Differences between "new features" and "improvements"
### 5.3 Differences between "new features" and "improvements"
The difference between a new feature and improvement can be quite fuzzy and the project owner reserves all rights to decide under which category your PR falls.
At a very basic level a PR is a new feature if it changes the intended way any part of the Red project currently works or if it modifies the user experience (UX) in any significant way. Otherwise, it is likely to be considered an improvement.
# Community
# 6. Community
You can chat with the core team and other community members about issues or pull requests in the #coding channel of the Red support server located [here](https://discord.gg/red).

29
.gitignore vendored
View File

@@ -1,40 +1,15 @@
# Trivia list repo injection
redbot/trivia/
*.json
*.exe
*.dll
.data
!/tests/cogs/dataconverter/data/**/*.json
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.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:
.idea/
*.iws
## Plugin-specific files:

View File

@@ -10,5 +10,4 @@ python:
version: 3.6
pip_install: true
extra_requirements:
- docs
- mongo
- mongo

View File

@@ -5,21 +5,29 @@ notifications:
email: false
python:
- 3.5.3
- 3.6.1
- 3.6.5
env:
global:
PIPENV_IGNORE_VIRTUALENVS=1
matrix:
- TOXENV=py
- TOXENV=docs
- TOXENV=style
install:
- echo "git+https://github.com/Rapptz/discord.py.git@rewrite#egg=discord.py[voice]" >> requirements.txt
- pip install -r requirements.txt
- pip install .[test]
- pip install --upgrade pip pipenv
- pipenv install --dev
script:
- python -m compileall ./redbot/cogs
- python -m pytest
- pipenv run tox
jobs:
include:
# These jobs only occur on tag creation for V3/develop if the prior ones succeed
- stage: PyPi Deployment
if: tag IS present
python: 3.5.3
python: 3.6.5
env:
- DEPLOYING=true
deploy:
@@ -31,14 +39,14 @@ jobs:
on:
repo: Cog-Creators/Red-DiscordBot
branch: V3/develop
python: 3.5.3
python: 3.6.5
tags: true
- stage: Crowdin Deployment
if: tag IS present
python: 3.5.3
python: 3.6.5
env:
- DEPLOYING=true
before_deployment:
before_deploy:
- curl https://artifacts.crowdin.com/repo/GPG-KEY-crowdin | sudo apt-key add -
- echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list
- sudo apt-get update -qq
@@ -50,5 +58,5 @@ jobs:
on:
repo: Cog-Creators/Red-DiscordBot
branch: V3/develop
python: 3.5.3
python: 3.6.5
tags: true

4
Makefile Normal file
View File

@@ -0,0 +1,4 @@
reformat:
black -l 99 `git ls-files "*.py"`
stylecheck:
black --check -l 99 `git ls-files "*.py"`

20
Pipfile Normal file
View File

@@ -0,0 +1,20 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
"discord.py" = { git = 'git://github.com/Rapptz/discord.py', ref = '7eb918b19e3e60b56eb9039eb267f8f3477c5e17', editable = true}
"e1839a8" = {path = ".", editable = true}
[dev-packages]
tox = "*"
pytest = "*"
pytest-asyncio = "*"
sphinx = ">1.7"
sphinxcontrib-asyncio = "*"
sphinx-rtd-theme = "*"
black = "*"
[pipenv]
allow_prereleases = true

452
Pipfile.lock generated Normal file
View File

@@ -0,0 +1,452 @@
{
"_meta": {
"hash": {
"sha256": "dcd688e81a2d0e793236e0335eb7cb9558d8b4acb66934afffcc0612cce2ec53"
},
"pipfile-spec": 6,
"requires": {},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"aiohttp": {
"hashes": [
"sha256:129d83dd067760cec3cfd4456b5c6d7ac29f2c639d856884568fd539bed5a51f",
"sha256:33c62afd115c456b0cf1e890fe6753055effe0f31a28321efd4f787378d6f4ab",
"sha256:666756e1d4cf161ed1486b82f65fdd386ac07dd20fb10f025abf4be54be12746",
"sha256:9705ded5a0faa25c8f14c6afb7044002d66c9120ed7eadb4aa9ca4aad32bd00c",
"sha256:af5bfdd164256118a0a306b3f7046e63207d1f8cba73a67dcc0bd858dcfcd3bc",
"sha256:b80f44b99fa3c9b4530fcfa324a99b84843043c35b084e0b653566049974435d",
"sha256:c67e105ec74b85c8cb666b6877569dee6f55b9548f982983b9bee80b3d47e6f3",
"sha256:d15c6658de5b7783c2538407278fa062b079a46d5f814a133ae0f09bbb2cfbc4",
"sha256:d611ebd1ef48498210b65486306e065fde031040a1f3c455ca1b6baa7bf32ad3",
"sha256:dcc7e4dcec6b0012537b9f8a0726f8b111188894ab0f924b680d40b13d3298a0",
"sha256:de8ef106e130b94ca143fdfc6f27cda1d8ba439462542377738af4d99d9f5dd2",
"sha256:eb6f1405b607fff7e44168e3ceb5d3c8a8c5a2d3effe0a27f843b16ec047a6d7",
"sha256:f0e2ac69cb709367400008cebccd5d48161dd146096a009a632a132babe5714c"
],
"version": "==2.2.5"
},
"aiohttp-json-rpc": {
"hashes": [
"sha256:9ec69ea70ce49c4af445f0ac56ac728708ccfad8b214272d2cc7e75bc0b31327",
"sha256:e2b8b49779d5d9b811f3a94e98092b1fa14af6d9adbf71c3afa6b20c641fa5d5"
],
"version": "==0.8.7"
},
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"async-timeout": {
"hashes": [
"sha256:00cff4d2dce744607335cba84e9929c3165632da2d27970dbc55802a0c7873d0",
"sha256:9093db5b8ddbe4b8f6885d1a6e0ad84ae3155464cbf6877c387605244c285f3c"
],
"version": "==2.0.1"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"colorama": {
"hashes": [
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
],
"version": "==0.3.9"
},
"discord.py": {
"editable": true,
"git": "git://github.com/Rapptz/discord.py",
"ref": "7eb918b19e3e60b56eb9039eb267f8f3477c5e17"
},
"distro": {
"hashes": [
"sha256:224041cef9600e72d19ae41ba006e71c05c4dc802516da715d7fda55ba3d8742",
"sha256:6ec8e539cf412830e5ccf521aecf879f2c7fcf60ce446e33cd16eef1ed8a0158"
],
"version": "==1.3.0"
},
"e1839a8": {
"editable": true,
"path": "."
},
"fuzzywuzzy": {
"hashes": [
"sha256:d40c22d2744dff84885b30bbfc07fab7875f641d070374331777a4d1808b8d4e",
"sha256:ecf490216fb4d76b558a03042ff8f45a8782f17326caca1384d834cbaa2c7e6f"
],
"version": "==0.16.0"
},
"idna": {
"hashes": [
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4"
],
"version": "==2.6"
},
"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"
],
"version": "==4.3.1"
},
"python-levenshtein": {
"hashes": [
"sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1"
],
"version": "==0.12.0"
},
"pyyaml": {
"hashes": [
"sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8",
"sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736",
"sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f",
"sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608",
"sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8",
"sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab",
"sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7",
"sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3",
"sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1",
"sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6",
"sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8",
"sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4",
"sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca",
"sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269"
],
"version": "==3.12"
},
"raven": {
"hashes": [
"sha256:0adae40e004dfe2181d1f2883aa3d4ca1cf16dbe449ae4b445b011c6eb220a90",
"sha256:84da75114739191bdf2388f296ffd6177e83567a7fbaf2701e034ad6026e4f3b"
],
"version": "==6.5.0"
},
"red-trivia": {
"hashes": [
"sha256:39413b9fb3f9b9362d6de1dcf69a4bf635b0f3518243f7178299b96d26cbb6a7"
],
"version": "==1.1.1"
},
"websockets": {
"hashes": [
"sha256:09dfec40e9b73e8808c39ecdbc1733e33915a2b26b90c54566afc0af546a9ec3",
"sha256:2aa6d52264cecb08d39741e8fda49f5ac4872aef02617230c84d02e861f3cc5a",
"sha256:2f5b7f3920f29609086fb0b63552bb1f86a04b8cbdcc0dbf3775cc90d489dfc8",
"sha256:3d38f76f71654268e5533b45df125ff208fee242a102d4b5ca958da5cf5fb345",
"sha256:3fcc7dfb365e81ff8206f950c86d1e73accdf3be2f9110c0cb73be32d2e7a9a5",
"sha256:4128212ab6f91afda03a0c697add261bdf6946b47928db83f07298ea2cd8d937",
"sha256:43e5b9f51dd0000a4c6f646e2ade0c886bd14a784ffac08b9e079bd17a63bcc5",
"sha256:4a932c17cb11c361c286c04842dc2385cc7157019bbba8b64808acbc89a95584",
"sha256:5ddc5fc121eb76771e990f071071d9530e27d20e8cfb804d9f5823de055837af",
"sha256:7347af28fcc70eb45be409760c2a428f8199e7f73c04a621916c3c219ed7ad27",
"sha256:85ae1e4b36aa2e90de56d211d2de36d7c093d00277a9afdd9b4f81e69c0214ab",
"sha256:8a29100079f5b91a72bcd25d35a7354db985d3babae42d00b9d629f9a0aaa8ac",
"sha256:a7e7585c8e3c0f9277ad7d6ee6ccddc69649cd216255d5e255d68f90482aeefa",
"sha256:aa42ecef3aed807e23218c264b1e82004cdd131a6698a10b57fc3d8af8f651fc",
"sha256:b19e7ede1ba80ee9de6f5b8ccd31beee25402e68bef7c13eeb0b8bc46bc4b7b7",
"sha256:c4c5b5ce2d66cb0cf193c14bc9726adca095febef0f7b2c04e5e3fa3487a97a4",
"sha256:de743ef26b002efceea7d7756e99e5d38bf5d4f27563b8d27df2a9a5cc57340a",
"sha256:e1e568136ad5cb6768504be36d470a136b072acbf3ea882303aee6361be01941",
"sha256:e8992f1db371f2a1c5af59e032d9dc7c1aa92f16241efcda695b7d955b4de0c2",
"sha256:e9c1cdbb591432c59d0b5ca64fd30b6d517024767f152fc169563b26e7bcc9da"
],
"version": "==3.4"
},
"yarl": {
"hashes": [
"sha256:605480ee43eead69ec8e8c52cdfefc79cef6379cc0e87d908cf290408c1e49af",
"sha256:7fad2530cb4ddf2b74c1e4f6f9f0e28eac482094c6542f98fd71ecf67fb4fded",
"sha256:837d866a70f1ea03005914a740bddea89a253afabd6589db981b91738768bd25",
"sha256:885e40812ff9fc80e6f28ef04ad6396e3ae583ab504b1a76301fdcec7fc9f30f",
"sha256:a5457e075eab1170141774a8c69906c223ea0088eaebd6ef91b04b33527fa905",
"sha256:baa0d3f7982fa0c03a55433109c405e79a597141f2e2d6ee7e16c03eabd74886",
"sha256:beeefbe0edd47fc8b657bf7bf44791f7a6e5b14f3de1846daf999687cb68c156",
"sha256:cf6a3d6fd3e79d3457d520c12d5d18b030d5ca5d0b205ca6481857804d8d944d",
"sha256:d07d3dc6849345b7437dc58ea49ad2a1960017386d86288550728ca38e482ddc",
"sha256:d81e45bedefccb97e4e8f7d32cfae0af1d9eadd1ae795fc420c8319c3dab2a28",
"sha256:e1da2853a92fbc7e2d0248bbfa931cd621121e70ce6dda7c1eeef3516d51b46c",
"sha256:f1201de3e93fb1efc3111c8928d9366875edefd65d77c0f6b847fe299e8e1122",
"sha256:fe0390a29b5c7e90975feefe863e3d3a851be546bd797b23f338d24a15efa920"
],
"version": "==0.18.0"
}
},
"develop": {
"alabaster": {
"hashes": [
"sha256:2eef172f44e8d301d25aff8068fddd65f767a3f04b5f15b0f4922f113aa1c732",
"sha256:37cdcb9e9954ed60912ebc1ca12a9d12178c26637abdf124e3cde2341c257fe0"
],
"version": "==0.7.10"
},
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"atomicwrites": {
"hashes": [
"sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585",
"sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6"
],
"version": "==1.1.5"
},
"attrs": {
"hashes": [
"sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265",
"sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b"
],
"version": "==18.1.0"
},
"babel": {
"hashes": [
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
],
"version": "==2.6.0"
},
"black": {
"hashes": [
"sha256:3efe92eafbde15f8ac06478de11cfb84e47504896ccdde64507e751d2f91ec3a",
"sha256:fc26c4ab28c541fb824f59fa83d5702f75829495d5a1dee603b29bc4fbe79095"
],
"index": "pypi",
"version": "==18.6b2"
},
"certifi": {
"hashes": [
"sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
"sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
],
"version": "==2018.4.16"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
],
"version": "==6.7"
},
"docutils": {
"hashes": [
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
],
"version": "==0.14"
},
"idna": {
"hashes": [
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4"
],
"version": "==2.6"
},
"imagesize": {
"hashes": [
"sha256:3620cc0cadba3f7475f9940d22431fc4d407269f1be59ec9b8edcca26440cf18",
"sha256:5b326e4678b6925158ccc66a9fa3122b6106d7c876ee32d7de6ce59385b96315"
],
"version": "==1.0.0"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
],
"version": "==1.0"
},
"more-itertools": {
"hashes": [
"sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8",
"sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3",
"sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0"
],
"version": "==4.2.0"
},
"packaging": {
"hashes": [
"sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0",
"sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b"
],
"version": "==17.1"
},
"pluggy": {
"hashes": [
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff",
"sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c",
"sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5"
],
"version": "==0.6.0"
},
"py": {
"hashes": [
"sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881",
"sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a"
],
"version": "==1.5.3"
},
"pygments": {
"hashes": [
"sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
"sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
],
"version": "==2.2.0"
},
"pyparsing": {
"hashes": [
"sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04",
"sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07",
"sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18",
"sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e",
"sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5",
"sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58",
"sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010"
],
"version": "==2.2.0"
},
"pytest": {
"hashes": [
"sha256:26838b2bc58620e01675485491504c3aa7ee0faf335c37fcd5f8731ca4319591",
"sha256:32c49a69566aa7c333188149ad48b58ac11a426d5352ea3d8f6ce843f88199cb"
],
"index": "pypi",
"version": "==3.6.1"
},
"pytest-asyncio": {
"hashes": [
"sha256:286b50773e996c80d894b95afaf45df6952408a67a59979ca9839f94693ec7fd",
"sha256:f32804bb58a66e13a3eda11f8942a71b1b6a30466b0d2ffe9214787aab0e172e"
],
"index": "pypi",
"version": "==0.8.0"
},
"pytz": {
"hashes": [
"sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555",
"sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749"
],
"version": "==2018.4"
},
"requests": {
"hashes": [
"sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
"sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
],
"version": "==2.18.4"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"snowballstemmer": {
"hashes": [
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
],
"version": "==1.2.1"
},
"sphinx": {
"hashes": [
"sha256:85f7e32c8ef07f4ba5aeca728e0f7717bef0789fba8458b8d9c5c294cad134f3",
"sha256:d45480a229edf70d84ca9fae3784162b1bc75ee47e480ffe04a4b7f21a95d76d"
],
"index": "pypi",
"version": "==1.7.5"
},
"sphinx-rtd-theme": {
"hashes": [
"sha256:aa3e190392e963551432de7df24b8a5fbe5b71a2f4fcd9d5b75808b52ad999e5",
"sha256:de88d637a60371d4f923e06b79c4ba260490c57d2ab5a8316942ab5d9a6ce1bf"
],
"index": "pypi",
"version": "==0.4.0"
},
"sphinxcontrib-asyncio": {
"hashes": [
"sha256:96627b1ec4eba08d09ad577ff9416c131910333ef37a2c82a2716e59646739f0"
],
"index": "pypi",
"version": "==0.2.0"
},
"sphinxcontrib-websupport": {
"hashes": [
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
],
"version": "==1.1.0"
},
"toml": {
"hashes": [
"sha256:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d"
],
"version": "==0.9.4"
},
"tox": {
"hashes": [
"sha256:96efa09710a3daeeb845561ebbe1497641d9cef2ee0aea30db6969058b2bda2f",
"sha256:9ee7de958a43806402a38c0d2aa07fa8553f4d2c20a15b140e9f771c2afeade0"
],
"index": "pypi",
"version": "==3.0.0"
},
"urllib3": {
"hashes": [
"sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
"sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
],
"version": "==1.22"
},
"virtualenv": {
"hashes": [
"sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669",
"sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752"
],
"version": "==16.0.0"
}
}
}

View File

@@ -1,42 +1,113 @@
.. image:: https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop
.. class:: center
.. image:: https://imgur.com/pY1WUFX.png
:target: https://github.com/Cog-Creators/Red-DiscordBot/tree/V3/develop
:alt: Red Discord Bot
.. class:: center
Music, Moderation, Trivia, Stream Alerts and fully customizable.
.. class:: center
.. image:: https://discordapp.com/api/guilds/133049272517001216/embed.png
:target: https://discord.gg/red
:alt: Discord server
.. image:: https://api.travis-ci.org/Cog-Creators/Red-DiscordBot.svg?branch=V3/develop
:target: https://travis-ci.org/Cog-Creators/Red-DiscordBot
:alt: Travis CI status
.. image:: https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop
:target: http://red-discordbot.readthedocs.io/en/v3-develop/?badge=v3-develop
:alt: Documentation Status
.. 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/PRs-welcome-brightgreen.svg?style=flat-square
:target: http://makeapullrequest.com
:alt: PRs Welcome
.. 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
.. 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
.. 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
********************
Red - Discord Bot v3
********************
==========
Overview
==========
**This is in beta and very much a work in progress. Regular use is not recommended.
There will not be any effort made to prevent the breaking of current installations.**
Red is a fully modular bot meaning all features and commands can be enabled/disabled to your liking, making it completely customizable.
This is also a *self-hosted bot* meaning you will need to host and maintain your own instance. You can turn Red into an admin bot, music bot, trivia bot, new best friend or all of these together!
How to install
^^^^^^^^^^^^^^
`Installation <#installation>`_ is easy, and you do **NOT** need to know anything about coding! Aside from installation and updating, every part of the bot can be controlled from within Discord.
Using python3 pip::
**The default set of modules includes and is not limited to:**
pip install --process-dependency-links -U Red-DiscordBot
redbot-setup
redbot <name>
- Moderation features (kick/ban/softban/hackban, mod-log, filter, chat cleanup)
- Trivia (lists are included and can be easily added)
- Music features (YouTube, SoundCloud, local files, playlists, queues)
- Stream alerts (Twitch, Youtube, Mixer, Hitbox, Picarto)
- Slot machine
- Custom commands
- Imgur/gif search
To install requirements for voice::
pip install --process-dependency-links -U Red-DiscordBot[voice]
**Additionally, other plugins (cogs) can be easily found and added from our growing community of cog repositories.**
To install all requirements for docs and tests::
- Cleverbot integration (talk to Red and she talks back)
- Ban sync
- Welcome messages
- Casino
- Reaction roles
- Slow Mode
- Anilist
- And much, much more!
pip install --process-dependency-links -U Red-DiscordBot[test,docs]
Feel free to take a `peek <https://github.com/Cog-Creators/Red-DiscordBot/issues/1398>`_!
For the latest git build, replace ``Red-DiscordBot`` in the above commands with
``git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop``.
==============
Installation
==============
**The following platforms are officially supported:**
- `Windows <https://red-discordbot.readthedocs.io/en/v3-develop/install_windows.html>`_
- `MacOS <https://red-discordbot.readthedocs.io/en/v3-develop/install_mac.html>`_
- `Ubuntu <https://red-discordbot.readthedocs.io/en/v3-develop/install_ubuntu.html>`_
- `Debian Stretch <https://red-discordbot.readthedocs.io/en/v3-develop/install_debian.html>`_
- `CentOS 7 <https://red-discordbot.readthedocs.io/en/v3-develop/install_centos.html>`_
- `Arch Linux <https://red-discordbot.readthedocs.io/en/v3-develop/install_arch.html>`_
- `Raspbian Stretch <https://red-discordbot.readthedocs.io/en/v3-develop/install_raspbian.html>`_
Already using **Red** V2? Take a look at the `Data Converter <https://red-discordbot.readthedocs.io/en/v3-develop/cog_dataconverter.html>`_ to import your data to V3.
If `after reading the guides <https://red-discordbot.readthedocs.io/en/v3-develop/>`_ you are still experiencing issues, feel free to join the `Official Server <https://discord.gg/red>`_ and ask in the **#support** channel for help.
=====================
Join the community!
=====================
**Red** is in continuous development, and its supported by an active community which produces new content (cogs/plugins) for everyone to enjoy. New features are constantly added. If you cant `find <https://github.com/Cog-Creators/Red-DiscordBot/issues/1398>`_ what youre looking for, consult our `guide <https://red-discordbot.readthedocs.io/en/v3-develop/guide_cog_creation.html>`_ on building your own cogs!
Join us on our `Official Discord Server <https://discord.gg/red>`_!
=========
License
=========
Released under the `GNU GPL v3 <#License>`_.
Red is named after the main character of "Transistor", a videogame by `Super Giant Games <https://www.supergiantgames.com/games/transistor/>`_
Artwork created by `Sinlaire <https://sinlaire.deviantart.com/>`_ on Deviant Art for the Red Bot Project.

View File

@@ -14,6 +14,9 @@ help:
.PHONY: help Makefile
init:
cd .. && pipenv lock -r --dev > docs/requirements.txt && echo 'git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py-1.0' >> docs/requirements.txt
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile

View File

@@ -1,12 +1,12 @@
.. systemd service guide
==========================
==============================================
Setting up auto-restart using systemd on Linux
==========================
==============================================
---------------------------
-------------------------
Creating the service file
---------------------------
-------------------------
Create the new service file:
@@ -27,15 +27,16 @@ Paste the following and replace all instances of :code:`username` with the usern
Type=idle
Restart=always
RestartSec=15
RestartPreventExitStatus=0
[Install]
WantedBy=multi-user.target
Save and exit :code:`ctrl + O; enter; ctrl + x`
---------------------------
---------------------------------
Starting and enabling the service
---------------------------
---------------------------------
.. note:: This same file can be used to start as many instances of the bot as you wish, without creating more service files, just start and enable more services and add any bot instance name after the **@**

107
docs/cog_permissions.rst Normal file
View File

@@ -0,0 +1,107 @@
.. Permissions Cog Reference
=========================
Permissions Cog Reference
=========================
------------
How it works
------------
When loaded, the permissions cog will allow you
to define extra custom rules for who can use a command
If no applicable rules are found, the command will behave as if
the cog was not loaded.
-------------
Rule priority
-------------
Rules set will be checked in the following order
1. Owner level command specific settings
2. Owner level cog specific settings
3. Server level command specific settings
4. Server level cog specific settings
For each of those, settings have varying priorities (listed below, highest to lowest priority)
1. User whitelist
2. User blacklist
3. Voice Channel whitelist
4. Voice Channel blacklist
5. Text Channel whitelist
6. Text Channel blacklist
7. Role settings (see below)
8. Server whitelist
9. Server blacklist
10. Default settings
For the role whitelist and blacklist settings,
roles will be checked individually in order from highest to lowest role the user has
Each role will be checked for whitelist, then blacklist. The first role with a setting
found will be the one used.
-------------------------
Setting Rules from a file
-------------------------
The permissions cog can set rules from a yaml file:
All entries are based on ID.
An example of the expected format is shown below.
.. code-block:: yaml
cogs:
Admin:
allow:
- 78631113035100160
deny:
- 96733288462286848
Audio:
allow:
- 133049272517001216
default: deny
commands:
cleanup bot:
allow:
- 78631113035100160
default: deny
ping:
deny:
- 96733288462286848
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

@@ -19,9 +19,10 @@
#
import os
import sys
sys.path.insert(0, os.path.abspath('..'))
os.environ['BUILDING_DOCS'] = "1"
sys.path.insert(0, os.path.abspath(".."))
os.environ["BUILDING_DOCS"] = "1"
# -- General configuration ------------------------------------------------
@@ -34,35 +35,36 @@ os.environ['BUILDING_DOCS'] = "1"
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
'sphinx.ext.napoleon',
'sphinxcontrib.asyncio'
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
"sphinx.ext.napoleon",
"sphinxcontrib.asyncio",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
source_suffix = ".rst"
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = 'Red - Discord Bot'
copyright = '2018, Cog Creators'
author = 'Cog Creators'
project = "Red - Discord Bot"
copyright = "2018, Cog Creators"
author = "Cog Creators"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
from redbot.core import __version__
# The short X.Y version.
version = __version__
# The full version, including alpha/beta/rc tags.
@@ -78,10 +80,10 @@ language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
@@ -95,7 +97,7 @@ default_role = "any"
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
html_theme = "sphinx_rtd_theme"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -105,16 +107,16 @@ html_theme = 'sphinx_rtd_theme'
html_context = {
# Enable the "Edit in GitHub link within the header of each page.
'display_github': True,
'github_user': 'Cog-Creators',
'github_repo': 'Red-DiscordBot',
'github_version': 'V3/develop/docs/'
"display_github": True,
"github_user": "Cog-Creators",
"github_repo": "Red-DiscordBot",
"github_version": "V3/develop/docs/",
}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
@@ -122,12 +124,12 @@ html_static_path = ['_static']
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = {
'**': [
'about.html',
'navigation.html',
'relations.html', # needs 'show_related': True theme option to display
'searchbox.html',
'donate.html',
"**": [
"about.html",
"navigation.html",
"relations.html", # needs 'show_related': True theme option to display
"searchbox.html",
"donate.html",
]
}
@@ -135,7 +137,7 @@ html_sidebars = {
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'Red-DiscordBotdoc'
htmlhelp_basename = "Red-DiscordBotdoc"
# -- Options for LaTeX output ---------------------------------------------
@@ -144,15 +146,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
@@ -162,8 +161,7 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'Red-DiscordBot.tex', 'Red - Discord Bot Documentation',
'Cog Creators', 'manual'),
(master_doc, "Red-DiscordBot.tex", "Red - Discord Bot Documentation", "Cog Creators", "manual")
]
@@ -171,10 +169,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'red-discordbot', 'Red - Discord Bot Documentation',
[author], 1)
]
man_pages = [(master_doc, "red-discordbot", "Red - Discord Bot Documentation", [author], 1)]
# -- Options for Texinfo output -------------------------------------------
@@ -183,15 +178,21 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Red-DiscordBot', 'Red - Discord Bot Documentation',
author, 'Red-DiscordBot', 'One line description of project.',
'Miscellaneous'),
(
master_doc,
"Red-DiscordBot",
"Red - Discord Bot Documentation",
author,
"Red-DiscordBot",
"One line description of project.",
"Miscellaneous",
)
]
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'python': ('https://docs.python.org/3.5', None),
'dpy': ('https://discordpy.readthedocs.io/en/rewrite/', None),
'motor': ('https://motor.readthedocs.io/en/stable/', None)}
intersphinx_mapping = {
"python": ("https://docs.python.org/3.6", None),
"dpy": ("https://discordpy.readthedocs.io/en/rewrite/", None),
"motor": ("https://motor.readthedocs.io/en/stable/", None),
}

View File

@@ -11,6 +11,10 @@ RedBase
.. autoclass:: RedBase
:members:
:exclude-members: get_context
.. automethod:: register_rpc_handler
.. automethod:: unregister_rpc_handler
Red
^^^

View File

@@ -4,8 +4,9 @@
Commands Package
================
This package acts almost identically to ``discord.ext.commands``; i.e. they both have the same
attributes. Some of these attributes, however, have been slightly modified, as outlined below.
This package acts almost identically to :doc:`discord.ext.commands <dpy:ext/commands/api>`; i.e.
they both have the same attributes. Some of these attributes, however, have been slightly modified,
as outlined below.
.. autofunction:: redbot.core.commands.command

View File

@@ -6,21 +6,35 @@ Downloader Framework
Info.json
*********
The info.json file may exist inside every package folder in the repo,
it is optional however. This string describes the valid keys within
an info file (and maybe how the Downloader cog uses them).
The optional info.json file may exist inside every package folder in the repo,
as well as in the root of the repo. The following sections describe the valid
keys within an info file (and maybe how the Downloader cog uses them).
KEYS (case sensitive):
Keys common to both repo and cog info.json (case sensitive)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- ``author`` (list of strings) - list of names of authors of the cog
- ``author`` (list of strings) - list of names of authors of the cog or repo.
- ``description`` (string) - A long description of the cog or repo. For cogs, this
is displayed when a user executes ``!cog info``.
- ``install_msg`` (string) - The message that gets displayed when a cog
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
is displayed when a user executes ``!cog list``
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)``
- ``description`` (string) - A long description of the cog that appears when a user executes ```!cog info``.
- ``hidden`` (bool) - Determines if a cog is visible in the cog list for a repo.
- ``hidden`` (bool) - Determines if a cog is available for install.
- ``install_msg`` (string) - The message that gets displayed when a cog is installed
- ``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.
Downloader will not deal with this functionality but it may be useful for other cogs.
@@ -29,9 +43,6 @@ KEYS (case sensitive):
passed to pip on cog install. ``SHARED_LIBRARIES`` do NOT go in this
list.
- ``short`` (string) - A short description of the cog that appears when
a user executes `!cog list`
- ``tags`` (list of strings) - A list of strings that are related to the
functionality of the cog. Used to aid in searching.

View File

@@ -4,5 +4,60 @@
RPC
===
.. automodule:: redbot.core.rpc
:members:
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.
Each of those functions must only take JSON serializable parameters and must return JSON serializable objects.
To enable the internal RPC server you must start the bot with the ``--rpc`` flag.
********
Examples
********
.. 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
*************
Please see the :class:`redbot.core.bot.RedBase` class for details on the RPC handler register and unregister methods.

View File

@@ -17,11 +17,10 @@ you in the process.
Getting started
---------------
To start off, be sure that you have installed Python 3.5 or higher (if you
are on Windows, stick with 3.5). Open a terminal or command prompt and type
To start off, be sure that you have installed Python 3.6 or higher. Open a terminal or command prompt and type
:code:`pip install --process-dependency-links -U git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=redbot[test]`
(note that if you get an error with this, try again but put :code:`python -m` in front of the command
This will install the latest version of V3.
This will install the latest version of V3.
--------------------
Setting up a package

View File

@@ -12,7 +12,8 @@ Welcome to Red - Discord Bot's documentation!
install_windows
install_mac
install_ubuntu
install_ubuntu_xenial
install_ubuntu_bionic
install_debian
install_centos
install_arch
@@ -25,6 +26,7 @@ Welcome to Red - Discord Bot's documentation!
:caption: Cog Reference:
cog_downloader
cog_permissions
.. toctree::
:maxdepth: 2

View File

@@ -14,8 +14,7 @@ Installing pre-requirements
yum -y groupinstall development
yum -y install https://centos7.iuscommunity.org/ius-release.rpm
yum -y install yum-utils wget which python35u python35u-pip python35u-devel openssl-devel libffi-devel git java-1.8.0-openjdk
sh -c "$(wget https://gist.githubusercontent.com/mustafaturan/7053900/raw/27f4c8bad3ee2bb0027a1a52dc8501bf1e53b270/latest-ffmpeg-centos6.sh -O -)"
yum -y install yum-utils wget which python36u python36u-pip python36u-devel openssl-devel libffi-devel git java-1.8.0-openjdk
--------------
Installing Red

View File

@@ -12,9 +12,24 @@ Installing pre-requirements
.. code-block:: none
echo "deb http://httpredir.debian.org/debian stretch-backports main contrib non-free" >> /etc/apt/sources.list
apt-get update
apt-get install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
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

View File

@@ -12,8 +12,24 @@ Installing pre-requirements
.. code-block:: none
sudo apt-get install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
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

View File

@@ -1,7 +1,7 @@
.. ubuntu install guide
.. ubuntu bionic install guide
==============================
Installing Red on Ubuntu 16.04
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>`_.
@@ -12,7 +12,7 @@ Installing the pre-requirements
.. code-block:: none
sudo apt install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
sudo apt install python3.6-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
------------------

View File

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

@@ -8,7 +8,7 @@ Installing Red on Windows
Needed Software
---------------
* `Python <https://python.org/downloads/>`_ - Red needs at least Python 3.5
* `Python <https://python.org/downloads/>`_ - Red needs Python 3.6
.. 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

View File

@@ -1,4 +1,37 @@
sphinx==1.6.5
sphinxcontrib-asyncio
sphinx_rtd_theme
git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py[voice]
-i https://pypi.org/simple
alabaster==0.7.10
appdirs==1.4.3
atomicwrites==1.1.5
attrs==18.1.0
babel==2.6.0
black==18.6b2
certifi==2018.4.16
chardet==3.0.4
click==6.7
docutils==0.14
idna==2.6
imagesize==1.0.0
jinja2==2.10
markupsafe==1.0
more-itertools==4.2.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.6.1
pytz==2018.4
requests==2.18.4
six==1.11.0
snowballstemmer==1.2.1
sphinx-rtd-theme==0.4.0
sphinx==1.7.5
sphinxcontrib-asyncio==0.2.0
sphinxcontrib-websupport==1.1.0
toml==0.9.4
tox==3.0.0
urllib3==1.22
virtualenv==16.0.0
yarl==0.18.0
git+https://github.com/Rapptz/discord.py@7eb918b19e3e60b56eb9039eb267f8f3477c5e17#egg=discord.py-1.0

14
generate_strings.py Normal file → Executable file
View File

@@ -1,3 +1,4 @@
#!/usr/bin/env python3
import subprocess
import os
import sys
@@ -12,22 +13,25 @@ def main():
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)
print(
f"Directory 'locales' exists for {d} but no 'regen_messages.py' is available!"
)
return 1
else:
print("Running 'regen_messages.py' for {}".format(d))
retval = subprocess.run([interpreter, "regen_messages.py"])
if retval.returncode != 0:
exit(1)
return 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)
return 1
os.chdir(root_dir)
subprocess.run(["crowdin", "upload"])
return 0
if __name__ == "__main__":
main()
sys.exit(main())

30
make.bat Normal file
View File

@@ -0,0 +1,30 @@
@echo off
if "%1"=="" goto help
REM This allows us to expand variables at execution
setlocal ENABLEDELAYEDEXPANSION
REM This will set PYFILES as a list of tracked .py files
set PYFILES=
for /F "tokens=* USEBACKQ" %%A in (`git ls-files "*.py"`) do (
set PYFILES=!PYFILES! %%A
)
goto %1
:reformat
black -l 99 !PYFILES!
exit /B %ERRORLEVEL%
:stylecheck
black -l 99 --check !PYFILES!
exit /B %ERRORLEVEL%
:help
echo Usage:
echo make ^<command^>
echo.
echo Commands:
echo reformat Reformat all .py files being tracked by git.
echo stylecheck Check which tracked .py files need reformatting.

View File

@@ -1,11 +1,14 @@
import sys
import typing
import discord
from colorama import init, Back
init()
# Let's do all the dumb version checking in one place.
if discord.version_info.major < 1:
print("You are not running the rewritten version of discord.py.\n\n"
"In order to use Red v3 you MUST be running d.py version"
" >= 1.0.0.")
print(
"You are not running the rewritten version of discord.py.\n\n"
"In order to use Red v3 you MUST be running d.py version"
" >= 1.0.0."
)
sys.exit(1)

View File

@@ -6,14 +6,14 @@ import sys
import discord
from redbot.core.bot import Red, ExitCodes
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.global_checks import init_global_checks
from redbot.core.events import init_events
from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry
from redbot.core.core_commands import Core
from redbot.core.dev_commands import Dev
from redbot.core import rpc, __version__
from redbot.core import __version__
import asyncio
import logging.handlers
import logging
@@ -40,24 +40,25 @@ def init_loggers(cli_flags):
logger = logging.getLogger("red")
red_format = logging.Formatter(
'%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: '
'%(message)s',
datefmt="[%d/%m/%Y %H:%M]")
"%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: %(message)s",
datefmt="[%d/%m/%Y %H:%M]",
)
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(red_format)
if cli_flags.debug:
os.environ['PYTHONASYNCIODEBUG'] = '1'
os.environ["PYTHONASYNCIODEBUG"] = "1"
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.WARNING)
from redbot.core.data_manager import core_data_path
logfile_path = core_data_path() / 'red.log'
logfile_path = core_data_path() / "red.log"
fhandler = logging.handlers.RotatingFileHandler(
filename=str(logfile_path), encoding='utf-8', mode='a',
maxBytes=10**7, backupCount=5)
filename=str(logfile_path), encoding="utf-8", mode="a", maxBytes=10 ** 7, backupCount=5
)
fhandler.setFormatter(red_format)
logger.addHandler(fhandler)
@@ -76,15 +77,17 @@ async def _get_prefix_and_token(red, indict):
:param indict:
:return:
"""
indict['token'] = await red.db.token()
indict['prefix'] = await red.db.prefix()
indict['enable_sentry'] = await red.db.enable_sentry()
indict["token"] = await red.db.token()
indict["prefix"] = await red.db.prefix()
indict["enable_sentry"] = await red.db.enable_sentry()
def list_instances():
if not config_file.exists():
print("No instances have been configured! Configure one "
"using `redbot-setup` before trying to run the bot!")
print(
"No instances have been configured! Configure one "
"using `redbot-setup` before trying to run the bot!"
)
sys.exit(1)
else:
data = JsonIO(config_file)._load_json()
@@ -103,12 +106,20 @@ def main():
elif cli_flags.version:
print(description)
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!")
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)
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_events(red, cli_flags)
red.add_cog(Core(red))
@@ -118,30 +129,35 @@ def main():
loop = asyncio.get_event_loop()
tmp_data = {}
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
token = os.environ.get("RED_TOKEN", tmp_data['token'])
prefix = cli_flags.prefix or tmp_data['prefix']
if token is None or not prefix:
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"]
if not (token and prefix):
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:
token = new_token
else:
log.critical("Token and prefix must be set in order to login.")
sys.exit(1)
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
if tmp_data['enable_sentry']:
if cli_flags.dry_run:
loop.run_until_complete(red.http.close())
sys.exit(0)
if tmp_data["enable_sentry"]:
red.enable_sentry()
cleanup_tasks = True
try:
loop.run_until_complete(red.start(token, bot=not cli_flags.not_bot))
except discord.LoginFailure:
cleanup_tasks = False # No login happened, no need for this
log.critical("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()
log.critical(
"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 = loop.run_until_complete(red.db.token())
if db_token and not cli_flags.no_prompt:
print("\nDo you want to reset the token? (y/n)")
if confirm("> "):
@@ -156,15 +172,16 @@ def main():
sentry_log.critical("Fatal Exception", exc_info=e)
loop.run_until_complete(red.logout())
finally:
rpc.clean_up()
if cleanup_tasks:
pending = asyncio.Task.all_tasks(loop=red.loop)
gathered = asyncio.gather(
*pending, loop=red.loop, return_exceptions=True)
gathered.cancel()
pending = asyncio.Task.all_tasks(loop=red.loop)
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
gathered.cancel()
try:
red.rpc.server.close()
except AttributeError:
pass
sys.exit(red._shutdown_mode.value)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -1,9 +1,8 @@
from typing import Tuple
import discord
from discord.ext import commands
from redbot.core import Config, checks
from redbot.core import Config, checks, commands
import logging
@@ -20,14 +19,14 @@ GENERIC_FORBIDDEN = (
HIERARCHY_ISSUE = (
"I tried to add {role.name} to {member.display_name} but that role"
" is higher than my highest role in the Discord heirarchy so I was"
" is higher than my highest role in the Discord hierarchy so I was"
" unable to successfully add it. Please give me a higher role and "
"try again."
)
USER_HIERARCHY_ISSUE = (
"I tried to add {role.name} to {member.display_name} but that role"
" is higher than your highest role in the Discord heirarchy so I was"
" is higher than your highest role in the Discord hierarchy so I was"
" unable to successfully add it. Please get a higher role and "
"try again."
)
@@ -41,17 +40,14 @@ RUNNING_ANNOUNCEMENT = (
class Admin:
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)
self.conf.register_global(
serverlocked=False
)
self.conf.register_global(serverlocked=False)
self.conf.register_guild(
announce_ignore=False,
announce_channel=None, # Integer ID
selfroles=[] # List of integer ID's
selfroles=[], # List of integer ID's
)
self.__current_announcer = None
@@ -63,8 +59,7 @@ class Admin:
pass
@staticmethod
async def complain(ctx: commands.Context, message: str,
**kwargs):
async def complain(ctx: commands.Context, message: str, **kwargs):
await ctx.send(message.format(**kwargs))
def is_announcing(self) -> bool:
@@ -78,8 +73,7 @@ class Admin:
return self.__current_announcer.active or False
@staticmethod
def pass_heirarchy_check(ctx: commands.Context,
role: discord.Role) -> bool:
def pass_hierarchy_check(ctx: commands.Context, role: discord.Role) -> bool:
"""
Determines if the bot has a higher role than the given one.
:param ctx:
@@ -89,8 +83,7 @@ class Admin:
return ctx.guild.me.top_role > role
@staticmethod
def pass_user_heirarchy_check(ctx: commands.Context,
role: discord.Role) -> bool:
def pass_user_hierarchy_check(ctx: commands.Context, role: discord.Role) -> bool:
"""
Determines if a user is allowed to add/remove/edit the given role.
:param ctx:
@@ -99,50 +92,47 @@ class Admin:
"""
return ctx.author.top_role > role
async def _addrole(self, ctx: commands.Context, member: discord.Member,
role: discord.Role):
async def _addrole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
try:
await member.add_roles(role)
except discord.Forbidden:
if not self.pass_heirarchy_check(ctx, role):
await self.complain(ctx, HIERARCHY_ISSUE, role=role,
member=member)
if not self.pass_hierarchy_check(ctx, role):
await self.complain(ctx, HIERARCHY_ISSUE, role=role, member=member)
else:
await self.complain(ctx, GENERIC_FORBIDDEN)
else:
await ctx.send("I successfully added {role.name} to"
" {member.display_name}".format(
role=role, member=member
))
await ctx.send(
"I successfully added {role.name} to"
" {member.display_name}".format(role=role, member=member)
)
async def _removerole(self, ctx: commands.Context, member: discord.Member,
role: discord.Role):
async def _removerole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
try:
await member.remove_roles(role)
except discord.Forbidden:
if not self.pass_heirarchy_check(ctx, role):
await self.complain(ctx, HIERARCHY_ISSUE, role=role,
member=member)
if not self.pass_hierarchy_check(ctx, role):
await self.complain(ctx, HIERARCHY_ISSUE, role=role, member=member)
else:
await self.complain(ctx, GENERIC_FORBIDDEN)
else:
await ctx.send("I successfully removed {role.name} from"
" {member.display_name}".format(
role=role, member=member
))
await ctx.send(
"I successfully removed {role.name} from"
" {member.display_name}".format(role=role, member=member)
)
@commands.command()
@commands.guild_only()
@checks.admin_or_permissions(manage_roles=True)
async def addrole(self, ctx: commands.Context, rolename: discord.Role, *,
user: MemberDefaultAuthor=None):
async def addrole(
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
author of the command.
Adds a role to a user.
If user is left blank it defaults to the author of the command.
"""
if user is None:
user = ctx.author
if self.pass_user_heirarchy_check(ctx, rolename):
if self.pass_user_hierarchy_check(ctx, rolename):
# noinspection PyTypeChecker
await self._addrole(ctx, user, rolename)
else:
@@ -151,15 +141,16 @@ class Admin:
@commands.command()
@commands.guild_only()
@checks.admin_or_permissions(manage_roles=True)
async def removerole(self, ctx: commands.Context, rolename: discord.Role, *,
user: MemberDefaultAuthor=None):
async def removerole(
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
author of the command.
Removes a role from a user.
If user is left blank it defaults to the author of the command.
"""
if user is None:
user = ctx.author
if self.pass_user_heirarchy_check(ctx, rolename):
if self.pass_user_hierarchy_check(ctx, rolename):
# noinspection PyTypeChecker
await self._removerole(ctx, user, rolename)
else:
@@ -170,12 +161,12 @@ class Admin:
@checks.admin_or_permissions(manage_roles=True)
async def editrole(self, ctx: commands.Context):
"""Edits roles settings"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
pass
@editrole.command(name="colour", aliases=["color", ])
async def editrole_colour(self, ctx: commands.Context, role: discord.Role,
value: discord.Colour):
@editrole.command(name="colour", aliases=["color"])
async def editrole_colour(
self, ctx: commands.Context, role: discord.Role, value: discord.Colour
):
"""Edits a role's colour
Use double quotes if the role contains spaces.
@@ -185,10 +176,9 @@ class Admin:
!editrole colour \"The Transistor\" #ff0000
!editrole colour Test #ff9900"""
author = ctx.author
reason = "{}({}) changed the colour of role '{}'".format(
author.name, author.id, role.name)
reason = "{}({}) changed the colour of role '{}'".format(author.name, author.id, role.name)
if not self.pass_user_heirarchy_check(ctx, role):
if not self.pass_user_hierarchy_check(ctx, role):
await self.complain(ctx, USER_HIERARCHY_ISSUE)
return
@@ -211,9 +201,10 @@ class Admin:
author = ctx.message.author
old_name = role.name
reason = "{}({}) changed the name of role '{}' to '{}'".format(
author.name, author.id, old_name, name)
author.name, author.id, old_name, name
)
if not self.pass_user_heirarchy_check(ctx, role):
if not self.pass_user_hierarchy_check(ctx, role):
await self.complain(ctx, USER_HIERARCHY_ISSUE)
return
@@ -240,8 +231,7 @@ class Admin:
await ctx.send("The announcement has begun.")
else:
prefix = ctx.prefix
await self.complain(ctx, RUNNING_ANNOUNCEMENT,
prefix=prefix)
await self.complain(ctx, RUNNING_ANNOUNCEMENT, prefix=prefix)
@announce.command(name="cancel")
@checks.is_owner()
@@ -259,7 +249,7 @@ class Admin:
@announce.command(name="channel")
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
async def announce_channel(self, ctx, *, channel: discord.TextChannel=None):
async def announce_channel(self, ctx, *, channel: discord.TextChannel = None):
"""
Changes the channel on which the bot makes announcements.
"""
@@ -267,29 +257,21 @@ class Admin:
channel = ctx.channel
await self.conf.guild(ctx.guild).announce_channel.set(channel.id)
await ctx.send("The announcement channel has been set to {}".format(
channel.mention
))
await ctx.send("The announcement channel has been set to {}".format(channel.mention))
@announce.command(name="ignore")
@commands.guild_only()
@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.
Defaults to the current server if none is provided.
Toggles whether the announcements will ignore the current server.
"""
if guild is None:
guild = ctx.guild
ignored = await self.conf.guild(guild).announce_ignore()
await self.conf.guild(guild).announce_ignore.set(not ignored)
ignored = await self.conf.guild(ctx.guild).announce_ignore()
await self.conf.guild(ctx.guild).announce_ignore.set(not ignored)
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]:
"""
@@ -309,11 +291,13 @@ class Admin:
# noinspection PyTypeChecker
return valid_roles
@commands.guild_only()
@commands.group(invoke_without_command=True)
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
"""
Add a role to yourself that server admins have configured as
user settable.
Add a role to yourself that server admins have configured as user settable.
NOTE: The role is case sensitive!
"""
# noinspection PyTypeChecker
await self._addrole(ctx, ctx.author, selfrole)
@@ -322,15 +306,19 @@ class Admin:
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
"""
Removes a selfrole from yourself.
NOTE: The role is case sensitive!
"""
# noinspection PyTypeChecker
await self._removerole(ctx, ctx.author, selfrole)
@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):
"""
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:
if role.id not in curr_selfroles:
@@ -339,10 +327,12 @@ class Admin:
await ctx.send("The selfroles list has been successfully modified.")
@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):
"""
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:
curr_selfroles.remove(role.id)
@@ -384,8 +374,10 @@ class Admin:
await ctx.send("The bot {} serverlocked.".format(verb))
# region Event Handlers
# region Event Handlers
async def on_guild_join(self, guild: discord.Guild):
if await self._serverlock_check(guild):
return
# endregion

View File

@@ -1,13 +1,11 @@
import asyncio
import discord
from discord.ext import commands
from redbot.core import commands
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 message:
@@ -65,10 +63,7 @@ class Announcer:
try:
await channel.send(self.message)
except discord.Forbidden:
await bot_owner.send("I could not announce to server: {}".format(
g.id
))
await bot_owner.send("I could not announce to server: {}".format(g.id))
await asyncio.sleep(0.5)
self.active = False

View File

@@ -1,5 +1,5 @@
import discord
from discord.ext import commands
from redbot.core import commands
class MemberDefaultAuthor(commands.Converter):
@@ -28,6 +28,5 @@ class SelfRole(commands.Converter):
role = await role_converter.convert(ctx, arg)
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

View File

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

View File

@@ -1,6 +1,6 @@
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))

View File

@@ -3,7 +3,7 @@ from re import search
from typing import Generator, Tuple, Iterable
import discord
from redbot.core import Config, commands
from redbot.core import Config, commands, checks
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box
@@ -26,14 +26,9 @@ class Alias:
and append them to the stored alias
"""
default_global_settings = {
"entries": []
}
default_global_settings = {"entries": []}
default_guild_settings = {
"enabled": False,
"entries": [] # Going to be a list of dicts
}
default_guild_settings = {"enabled": False, "entries": []} # Going to be a list of dicts
def __init__(self, bot: Red):
self.bot = bot
@@ -49,14 +44,17 @@ class Alias:
return (AliasEntry.from_json(d) for d in (await self._aliases.entries()))
async def loaded_aliases(self, guild: discord.Guild) -> Generator[AliasEntry, None, None]:
return (AliasEntry.from_json(d, bot=self.bot)
for d in (await self._aliases.guild(guild).entries()))
return (
AliasEntry.from_json(d, bot=self.bot)
for d in (await self._aliases.guild(guild).entries())
)
async def loaded_global_aliases(self) -> Generator[AliasEntry, None, None]:
return (AliasEntry.from_json(d, bot=self.bot) for d in (await self._aliases.entries()))
async def is_alias(self, guild: discord.Guild, alias_name: str,
server_aliases: Iterable[AliasEntry]=()) -> (bool, AliasEntry):
async def is_alias(
self, guild: discord.Guild, alias_name: str, server_aliases: Iterable[AliasEntry] = ()
) -> (bool, AliasEntry):
if not server_aliases:
server_aliases = await self.unloaded_aliases(guild)
@@ -76,10 +74,11 @@ class Alias:
@staticmethod
def is_valid_alias_name(alias_name: str) -> bool:
return not bool(search(r'\s', alias_name)) and alias_name.isprintable()
return not bool(search(r"\s", alias_name)) and alias_name.isprintable()
async def add_alias(self, ctx: commands.Context, alias_name: str,
command: Tuple[str], global_: bool=False) -> AliasEntry:
async def add_alias(
self, ctx: commands.Context, alias_name: str, command: Tuple[str], global_: bool = False
) -> AliasEntry:
alias = AliasEntry(alias_name, command, ctx.author, global_=global_)
if global_:
@@ -93,8 +92,9 @@ class Alias:
return alias
async def delete_alias(self, ctx: commands.Context, alias_name: str,
global_: bool=False) -> bool:
async def delete_alias(
self, ctx: commands.Context, alias_name: str, global_: bool = False
) -> bool:
if global_:
settings = self._aliases
else:
@@ -120,16 +120,15 @@ class Alias:
"""
content = message.content
prefix_list = await self.bot.command_prefix(self.bot, message)
prefixes = sorted(prefix_list,
key=lambda pfx: len(pfx),
reverse=True)
prefixes = sorted(prefix_list, key=lambda pfx: len(pfx), reverse=True)
for p in prefixes:
if content.startswith(p):
return p
raise ValueError(_("No prefix found."))
def get_extra_args_from_alias(self, message: discord.Message, prefix: str,
alias: AliasEntry) -> str:
def get_extra_args_from_alias(
self, message: discord.Message, prefix: str, alias: AliasEntry
) -> str:
"""
When an alias is executed by a user in chat this function tries
to get any extra arguments passed in with the call.
@@ -143,25 +142,27 @@ class Alias:
extra = message.content[known_content_length:].strip()
return extra
async def maybe_call_alias(self, message: discord.Message,
aliases: Iterable[AliasEntry]=None):
async def maybe_call_alias(
self, message: discord.Message, aliases: Iterable[AliasEntry] = None
):
try:
prefix = await self.get_prefix(message)
except ValueError:
return
try:
potential_alias = message.content[len(prefix):].split(" ")[0]
potential_alias = message.content[len(prefix) :].split(" ")[0]
except IndexError:
return False
is_alias, alias = await self.is_alias(message.guild, potential_alias, server_aliases=aliases)
is_alias, alias = await self.is_alias(
message.guild, potential_alias, server_aliases=aliases
)
if is_alias:
await self.call_alias(message, prefix, alias)
async def call_alias(self, message: discord.Message, prefix: str,
alias: AliasEntry):
async def call_alias(self, message: discord.Message, prefix: str, alias: AliasEntry):
new_message = copy(message)
args = self.get_extra_args_from_alias(message, prefix, alias)
@@ -173,97 +174,118 @@ class Alias:
@commands.guild_only()
async def alias(self, ctx: commands.Context):
"""Manage per-server aliases for commands"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
pass
@alias.group(name="global")
async def global_(self, ctx: commands.Context):
"""
Manage global aliases.
"""
if ctx.invoked_subcommand is None or \
isinstance(ctx.invoked_subcommand, commands.Group):
await ctx.send_help()
pass
@checks.mod_or_permissions(manage_guild=True)
@alias.command(name="add")
@commands.guild_only()
async def _add_alias(self, ctx: commands.Context,
alias_name: str, *, command):
async def _add_alias(self, ctx: commands.Context, alias_name: str, *, command):
"""
Add an alias for a command.
"""
# region Alias Add Validity Checking
# region Alias Add Validity Checking
is_command = self.is_command(alias_name)
if is_command:
await ctx.send(_("You attempted to create a new alias"
" with the name {} but that"
" name is already a command on this bot.").format(alias_name))
await ctx.send(
_(
"You attempted to create a new alias"
" with the name {} but that"
" name is already a command on this bot."
).format(alias_name)
)
return
is_alias, something_useless = await self.is_alias(ctx.guild, alias_name)
if is_alias:
await ctx.send(_("You attempted to create a new alias"
" with the name {} but that"
" alias already exists on this server.").format(alias_name))
await ctx.send(
_(
"You attempted to create a new alias"
" with the name {} but that"
" alias already exists on this server."
).format(alias_name)
)
return
is_valid_name = self.is_valid_alias_name(alias_name)
if not is_valid_name:
await ctx.send(_("You attempted to create a new alias"
" with the name {} but that"
" name is an invalid alias name. Alias"
" names may not contain spaces.").format(alias_name))
await ctx.send(
_(
"You attempted to create a new alias"
" with the name {} but that"
" name is an invalid alias name. Alias"
" names may not contain spaces."
).format(alias_name)
)
return
# endregion
# endregion
# At this point we know we need to make a new alias
# and that the alias name is valid.
await self.add_alias(ctx, alias_name, command)
await ctx.send(_("A new alias with the trigger `{}`"
" has been created.").format(alias_name))
await ctx.send(_("A new alias with the trigger `{}` has been created.").format(alias_name))
@checks.is_owner()
@global_.command(name="add")
async def _add_global_alias(self, ctx: commands.Context,
alias_name: str, *, command):
async def _add_global_alias(self, ctx: commands.Context, alias_name: str, *, command):
"""
Add a global alias for a command.
"""
# region Alias Add Validity Checking
# region Alias Add Validity Checking
is_command = self.is_command(alias_name)
if is_command:
await ctx.send(_("You attempted to create a new global alias"
" with the name {} but that"
" name is already a command on this bot.").format(alias_name))
await ctx.send(
_(
"You attempted to create a new global alias"
" with the name {} but that"
" name is already a command on this bot."
).format(alias_name)
)
return
is_alias, something_useless = await self.is_alias(ctx.guild, alias_name)
if is_alias:
await ctx.send(_("You attempted to create a new global alias"
" with the name {} but that"
" alias already exists on this server.").format(alias_name))
await ctx.send(
_(
"You attempted to create a new global alias"
" with the name {} but that"
" alias already exists on this server."
).format(alias_name)
)
return
is_valid_name = self.is_valid_alias_name(alias_name)
if not is_valid_name:
await ctx.send(_("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.").format(alias_name))
await ctx.send(
_(
"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."
).format(alias_name)
)
return
# endregion
# endregion
await self.add_alias(ctx, alias_name, command, global_=True)
await ctx.send(_("A new global alias with the trigger `{}`"
" has been created.").format(alias_name))
await ctx.send(
_("A new global alias with the trigger `{}` has been created.").format(alias_name)
)
@alias.command(name="help")
@commands.guild_only()
async def _help_alias(self, ctx: commands.Context, alias_name: str):
"""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:
base_cmd = alias.command[0]
@@ -280,11 +302,13 @@ class Alias:
is_alias, alias = await self.is_alias(ctx.guild, alias_name)
if is_alias:
await ctx.send(_("The `{}` alias will execute the"
" command `{}`").format(alias_name, alias.command))
await ctx.send(
_("The `{}` alias will execute the command `{}`").format(alias_name, alias.command)
)
else:
await ctx.send(_("There is no alias with the name `{}`").format(alias_name))
@checks.mod_or_permissions(manage_guild=True)
@alias.command(name="del")
@commands.guild_only()
async def _del_alias(self, ctx: commands.Context, alias_name: str):
@@ -299,11 +323,13 @@ class Alias:
return
if await self.delete_alias(ctx, alias_name):
await ctx.send(_("Alias with the name `{}` was successfully"
" deleted.").format(alias_name))
await ctx.send(
_("Alias with the name `{}` was successfully deleted.").format(alias_name)
)
else:
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
@checks.is_owner()
@global_.command(name="del")
async def _del_global_alias(self, ctx: commands.Context, alias_name: str):
"""
@@ -317,8 +343,9 @@ class Alias:
return
if await self.delete_alias(ctx, alias_name, global_=True):
await ctx.send(_("Alias with the name `{}` was successfully"
" deleted.").format(alias_name))
await ctx.send(
_("Alias with the name `{}` was successfully deleted.").format(alias_name)
)
else:
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
@@ -328,7 +355,9 @@ class Alias:
"""
Lists the available aliases on this server.
"""
names = [_("Aliases:"), ] + sorted(["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))])
names = [_("Aliases:")] + sorted(
["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))]
)
if len(names) == 0:
await ctx.send(_("There are no aliases on this server."))
else:
@@ -339,7 +368,9 @@ class Alias:
"""
Lists the available global aliases on this bot.
"""
names = [_("Aliases:"), ] + sorted(["+ " + a.name for a in await self.unloaded_global_aliases()])
names = [_("Aliases:")] + sorted(
["+ " + a.name for a in await self.unloaded_global_aliases()]
)
if len(names) == 0:
await ctx.send(_("There are no aliases on this server."))
else:

View File

@@ -5,8 +5,9 @@ from redbot.core import commands
class AliasEntry:
def __init__(self, name: str, command: Tuple[str],
creator: discord.Member, global_: bool=False):
def __init__(
self, name: str, command: Tuple[str], creator: discord.Member, global_: bool = False
):
super().__init__()
self.has_real_data = False
self.name = name
@@ -43,13 +44,12 @@ class AliasEntry:
"creator": creator,
"guild": guild,
"global": self.global_,
"uses": self.uses
"uses": self.uses,
}
@classmethod
def from_json(cls, data: dict, bot: commands.Bot=None):
ret = cls(data["name"], data["command"],
data["creator"], global_=data["global"])
def from_json(cls, data: dict, bot: commands.Bot = None):
ret = cls(data["name"], data["command"], data["creator"], global_=data["global"])
if bot:
ret.has_real_data = True

View File

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

View File

@@ -1,16 +1,18 @@
from pathlib import Path
from aiohttp import ClientSession
import shutil
import logging
from .audio import Audio
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
import redbot.core
log = logging.getLogger("red.audio")
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__)
LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio")
@@ -21,7 +23,7 @@ BUNDLED_APP_YML_FILE = Path(__file__).parent / "application.yml"
async def download_lavalink(session):
with LAVALINK_JAR_FILE.open(mode='wb') as f:
with LAVALINK_JAR_FILE.open(mode="wb") as f:
async with session.get(LAVALINK_DOWNLOAD_URL) as resp:
while True:
chunk = await resp.content.read(512)
@@ -34,15 +36,13 @@ async def maybe_download_lavalink(loop, cog):
jar_exists = LAVALINK_JAR_FILE.exists()
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:
log.info("Downloading Lavalink.jar")
LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
await download_lavalink(session)
async with ClientSession(loop=loop) as session:
await download_lavalink(session)
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))
@@ -53,4 +53,5 @@ async def setup(bot: commands.Bot):
await start_lavalink_server(bot.loop)
bot.add_cog(cog)
bot.loop.create_task(cog.disconnect_timer())
bot.loop.create_task(cog.init_config())

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -4,8 +4,11 @@ import asyncio
from subprocess import Popen, DEVNULL, PIPE
import os
import logging
from typing import Optional, Tuple
log = logging.getLogger('red.audio.manager')
_JavaVersion = Tuple[int, int]
log = logging.getLogger("red.audio.manager")
proc = None
SHUTDOWN = asyncio.Event()
@@ -13,7 +16,8 @@ SHUTDOWN = asyncio.Event()
def has_java_error(pid):
from . import LAVALINK_DOWNLOAD_DIR
poss_error_file = LAVALINK_DOWNLOAD_DIR / 'hs_err_pid{}.log'.format(pid)
poss_error_file = LAVALINK_DOWNLOAD_DIR / "hs_err_pid{}.log".format(pid)
return poss_error_file.exists()
@@ -29,39 +33,37 @@ async def monitor_lavalink_server(loop):
log.info("Restarting Lavalink jar.")
await start_lavalink_server(loop)
else:
log.error("Your Java is borked. Please find the hs_err_pid{}.log file"
" in the Audio data folder and report this issue.".format(
proc.pid
))
log.error(
"Your Java is borked. Please find the hs_err_pid{}.log file"
" in the Audio data folder and report this issue.".format(proc.pid)
)
async def has_java(loop):
java_available = shutil.which('java') is not None
async def has_java(loop) -> Tuple[bool, Optional[_JavaVersion]]:
java_available = shutil.which("java") is not None
if not java_available:
return False
return False, None
version = await get_java_version(loop)
return version >= (1, 8), version
async def get_java_version(loop):
async def get_java_version(loop) -> _JavaVersion:
"""
This assumes we've already checked that java exists.
"""
proc = Popen(
shlex.split("java -version", posix=os.name == 'posix'),
stdout=PIPE, stderr=PIPE
)
proc = Popen(shlex.split("java -version", posix=os.name == "posix"), stdout=PIPE, stderr=PIPE)
_, err = proc.communicate()
version_info = str(err, encoding='utf-8')
version_info = str(err, encoding="utf-8")
version_line = version_info.split('\n')[0]
version_line = version_info.split("\n")[0]
version_start = version_line.find('"')
version_string = version_line[version_start + 1:-1]
major, minor = version_string.split('.')[:2]
version_string = version_line[version_start + 1 : -1]
major, minor = version_string.split(".")[:2]
return int(major), int(minor)
async def start_lavalink_server(loop):
java_available, java_version = await has_java(loop)
if not java_available:
@@ -72,13 +74,15 @@ async def start_lavalink_server(loop):
extra_flags = "-Dsun.zip.disableMemoryMapping=true"
from . import LAVALINK_DOWNLOAD_DIR, LAVALINK_JAR_FILE
start_cmd = "java {} -jar {}".format(extra_flags, LAVALINK_JAR_FILE.resolve())
global proc
proc = Popen(
shlex.split(start_cmd, posix=os.name == 'posix'),
shlex.split(start_cmd, posix=os.name == "posix"),
cwd=str(LAVALINK_DOWNLOAD_DIR),
stdout=DEVNULL, stderr=DEVNULL
stdout=DEVNULL,
stderr=DEVNULL,
)
log.info("Lavalink jar started. PID: {}".format(proc.pid))

View File

@@ -6,7 +6,7 @@ from redbot.core.i18n import Translator, cog_i18n
from redbot.core.bot import Red # Only used for type hints
_ = Translator('Bank', __file__)
_ = Translator("Bank", __file__)
def check_global_setting_guildowner():
@@ -14,15 +14,18 @@ def check_global_setting_guildowner():
Command decorator. If the bank is not global, it checks if the author is
either the guildowner or has the administrator permission.
"""
async def pred(ctx: commands.Context):
author = ctx.author
if await ctx.bot.is_owner(author):
return True
if not await bank.is_global():
if not isinstance(ctx.channel, discord.abc.GuildChannel):
return False
if await ctx.bot.is_owner(author):
return True
permissions = ctx.channel.permissions_for(author)
return author == ctx.guild.owner or permissions.administrator
else:
return await ctx.bot.is_owner(author)
return commands.check(pred)
@@ -32,17 +35,20 @@ def check_global_setting_admin():
Command decorator. If the bank is not global, it checks if the author is
either a bot admin or has the manage_guild permission.
"""
async def pred(ctx: commands.Context):
author = ctx.author
if await ctx.bot.is_owner(author):
return True
if not await bank.is_global():
if not isinstance(ctx.channel, discord.abc.GuildChannel):
return False
if await ctx.bot.is_owner(author):
return True
permissions = ctx.channel.permissions_for(author)
is_guild_owner = author == ctx.guild.owner
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
else:
return await ctx.bot.is_owner(author)
return commands.check(pred)
@@ -56,8 +62,9 @@ class Bank:
# SECTION commands
@commands.group()
@check_global_setting_guildowner()
@checks.guildowner_or_permissions(administrator=True)
@commands.group(autohelp=True)
async def bankset(self, ctx: commands.Context):
"""Base command for bank settings"""
if ctx.invoked_subcommand is None:
@@ -67,25 +74,19 @@ class Bank:
default_balance = await bank._conf.default_balance()
else:
if not ctx.guild:
await ctx.send_help()
return
bank_name = await bank._conf.guild(ctx.guild).bank_name()
currency_name = await bank._conf.guild(ctx.guild).currency()
default_balance = await bank._conf.guild(ctx.guild).default_balance()
settings = (_(
"Bank settings:\n\n"
"Bank name: {}\n"
"Currency: {}\n"
"Default balance: {}"
"").format(bank_name, currency_name, default_balance)
)
settings = _(
"Bank settings:\n\nBank name: {}\nCurrency: {}\nDefault balance: {}"
).format(bank_name, currency_name, default_balance)
await ctx.send(box(settings))
await ctx.send_help()
@bankset.command(name="toggleglobal")
@checks.is_owner()
async def bankset_toggleglobal(self, ctx: commands.Context, confirm: bool=False):
async def bankset_toggleglobal(self, ctx: commands.Context, confirm: bool = False):
"""Toggles whether the bank is global or not
If the bank is global, it will become per-server
If the bank is per-server, it will become global"""
@@ -94,10 +95,10 @@ class Bank:
word = _("per-server") if cur_setting else _("global")
if confirm is False:
await ctx.send(
_("This will toggle the bank to be {}, deleting all accounts "
"in the process! If you're sure, type `{}`").format(
word, "{}bankset toggleglobal yes".format(ctx.prefix)
)
_(
"This will toggle the bank to be {}, deleting all accounts "
"in the process! If you're sure, type `{}`"
).format(word, "{}bankset toggleglobal yes".format(ctx.prefix))
)
else:
await bank.set_global(not cur_setting)

View File

@@ -1,6 +1,7 @@
class BankError(Exception):
pass
class BankNotGlobal(BankError):
pass
@@ -34,4 +35,4 @@ class NegativeValue(BankError):
class SameSenderAndReceiver(BankError):
pass
pass

View File

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

View File

@@ -27,13 +27,16 @@ class Cleanup:
Tries its best to cleanup after itself if the response is positive.
"""
def author_check(message):
return message.author == ctx.author
prompt = await ctx.send(_('Are you sure you want to delete {} messages? (y/n)').format(number))
response = await ctx.bot.wait_for('message', check=author_check)
prompt = await ctx.send(
_("Are you sure you want to delete {} messages? (y/n)").format(number)
)
response = await ctx.bot.wait_for("message", check=author_check)
if response.content.lower().startswith('y'):
if response.content.lower().startswith("y"):
await prompt.delete()
try:
await response.delete()
@@ -41,14 +44,19 @@ class Cleanup:
pass
return True
else:
await ctx.send(_('Cancelled.'))
await ctx.send(_("Cancelled."))
return False
@staticmethod
async def get_messages_for_deletion(
ctx: commands.Context, channel: discord.TextChannel, number,
check=lambda x: True, limit=100, before=None, after=None,
delete_pinned=False
ctx: commands.Context,
channel: discord.TextChannel,
number,
check=lambda x: True,
limit=100,
before=None,
after=None,
delete_pinned=False,
) -> list:
"""
Gets a list of messages meeting the requirements to be deleted.
@@ -63,13 +71,11 @@ class Cleanup:
to_delete = []
too_old = False
while not too_old and len(to_delete) - 1 < number:
while not too_old and len(to_delete) < number:
message = None
async for message in channel.history(limit=limit,
before=before,
after=after):
async for message in channel.history(limit=limit, before=before, after=after):
if (
(not number or len(to_delete) - 1 < number)
(not number or len(to_delete) < number)
and check(message)
and (ctx.message.created_at - message.created_at).days < 14
and (delete_pinned or not message.pinned)
@@ -90,13 +96,13 @@ class Cleanup:
@checks.mod_or_permissions(manage_messages=True)
async def cleanup(self, ctx: commands.Context):
"""Deletes messages."""
if ctx.invoked_subcommand is None:
await ctx.send_help()
pass
@cleanup.command()
@commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def text(self, ctx: commands.Context, text: str, number: int, delete_pinned: bool=False):
async def text(
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
):
"""Deletes last X messages matching the specified text.
Example:
@@ -105,6 +111,10 @@ class Cleanup:
Remember to use double quotes."""
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
is_bot = self.bot.user.bot
@@ -122,12 +132,18 @@ class Cleanup:
return False
to_delete = await self.get_messages_for_deletion(
ctx, channel, number, check=check, limit=1000, before=ctx.message,
delete_pinned=delete_pinned)
ctx,
channel,
number,
check=check,
limit=1000,
before=ctx.message,
delete_pinned=delete_pinned,
)
reason = "{}({}) deleted {} messages "\
" containing '{}' in channel {}.".format(author.name,
author.id, len(to_delete), text, channel.id)
reason = "{}({}) deleted {} messages containing '{}' in channel {}.".format(
author.name, author.id, len(to_delete), text, channel.id
)
log.info(reason)
if is_bot:
@@ -137,13 +153,18 @@ class Cleanup:
@cleanup.command()
@commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def user(self, ctx: commands.Context, user: str, number: int, delete_pinned: bool=False):
async def user(
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
):
"""Deletes last X messages from specified user.
Examples:
cleanup user @\u200bTwentysix 2
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
try:
@@ -156,7 +177,6 @@ class Cleanup:
else:
_id = member.id
channel = ctx.channel
author = ctx.author
is_bot = self.bot.user.bot
@@ -174,13 +194,19 @@ class Cleanup:
return False
to_delete = await self.get_messages_for_deletion(
ctx, channel, number, check=check, limit=1000, before=ctx.message,
delete_pinned=delete_pinned
ctx,
channel,
number,
check=check,
limit=1000,
before=ctx.message,
delete_pinned=delete_pinned,
)
reason = (
"{}({}) deleted {} messages "
" made by {}({}) in channel {}."
"".format(author.name, author.id, len(to_delete), member or "???", _id, channel.name)
)
reason = "{}({}) deleted {} messages "\
" made by {}({}) in channel {}."\
"".format(author.name, author.id, len(to_delete),
member or '???', _id, channel.name)
log.info(reason)
if is_bot:
@@ -191,8 +217,7 @@ class Cleanup:
@cleanup.command()
@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.
To get a message id, enable developer mode in Discord's
@@ -203,12 +228,14 @@ class Cleanup:
"""
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
is_bot = self.bot.user.bot
if not is_bot:
await ctx.send(_("This command can only be used on bots with "
"bot accounts."))
await ctx.send(_("This command can only be used on bots with bot accounts."))
return
after = await channel.get_message(message_id)
@@ -221,23 +248,25 @@ class Cleanup:
ctx, channel, 0, limit=None, after=after, delete_pinned=delete_pinned
)
reason = "{}({}) deleted {} messages in channel {}."\
"".format(author.name, author.id,
len(to_delete), channel.name)
reason = "{}({}) deleted {} messages in channel {}.".format(
author.name, author.id, len(to_delete), channel.name
)
log.info(reason)
await mass_purge(to_delete, channel)
@cleanup.command()
@commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool=False):
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
"""Deletes last X messages.
Example:
cleanup messages 26"""
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
is_bot = self.bot.user.bot
@@ -248,14 +277,13 @@ class Cleanup:
return
to_delete = await self.get_messages_for_deletion(
ctx, channel, number, limit=1000, before=ctx.message,
delete_pinned=delete_pinned
ctx, channel, number, limit=1000, before=ctx.message, delete_pinned=delete_pinned
)
to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages in channel {}."\
"".format(author.name, author.id,
number, channel.name)
reason = "{}({}) deleted {} messages in channel {}.".format(
author.name, author.id, number, channel.name
)
log.info(reason)
if is_bot:
@@ -263,13 +291,15 @@ class Cleanup:
else:
await slow_deletion(to_delete)
@cleanup.command(name='bot')
@cleanup.command(name="bot")
@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."""
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
is_bot = self.bot.user.bot
@@ -278,13 +308,13 @@ class Cleanup:
if not cont:
return
prefixes = await self.bot.get_prefix(ctx.message) # This returns all server prefixes
prefixes = await self.bot.get_prefix(ctx.message) # This returns all server prefixes
if isinstance(prefixes, str):
prefixes = [prefixes]
# In case some idiot sets a null prefix
if '' in prefixes:
prefixes.remove('')
if "" in prefixes:
prefixes.remove("")
def check(m):
if m.author.id == self.bot.user.id:
@@ -293,20 +323,26 @@ class Cleanup:
return True
p = discord.utils.find(m.content.startswith, prefixes)
if p and len(p) > 0:
cmd_name = m.content[len(p):].split(' ')[0]
cmd_name = m.content[len(p) :].split(" ")[0]
return bool(self.bot.get_command(cmd_name))
return False
to_delete = await self.get_messages_for_deletion(
ctx, channel, number, check=check, limit=1000, before=ctx.message,
delete_pinned=delete_pinned
ctx,
channel,
number,
check=check,
limit=1000,
before=ctx.message,
delete_pinned=delete_pinned,
)
to_delete.append(ctx.message)
reason = "{}({}) deleted {} "\
" command messages in channel {}."\
"".format(author.name, author.id, len(to_delete),
channel.name)
reason = (
"{}({}) deleted {} "
" command messages in channel {}."
"".format(author.name, author.id, len(to_delete), channel.name)
)
log.info(reason)
if is_bot:
@@ -314,10 +350,14 @@ class Cleanup:
else:
await slow_deletion(to_delete)
@cleanup.command(name='self')
@cleanup.command(name="self")
async def cleanup_self(
self, ctx: commands.Context, number: int,
match_pattern: str = None, delete_pinned: bool=False):
self,
ctx: commands.Context,
number: int,
match_pattern: str = None,
delete_pinned: bool = False,
):
"""Cleans up messages owned by the bot.
By default, all messages are cleaned. If a third argument is specified,
@@ -343,8 +383,7 @@ class Cleanup:
me = ctx.guild.me
can_mass_purge = channel.permissions_for(me).manage_messages
use_re = (match_pattern and match_pattern.startswith('r(') and
match_pattern.endswith(')'))
use_re = match_pattern and match_pattern.startswith("r(") and match_pattern.endswith(")")
if use_re:
match_pattern = match_pattern[1:] # strip 'r'
@@ -352,10 +391,14 @@ class Cleanup:
def content_match(c):
return bool(match_re.match(c))
elif match_pattern:
def content_match(c):
return match_pattern in c
else:
def content_match(_):
return True
@@ -367,23 +410,29 @@ class Cleanup:
return False
to_delete = await self.get_messages_for_deletion(
ctx, channel, number, check=check, limit=1000, before=ctx.message,
delete_pinned=delete_pinned
ctx,
channel,
number,
check=check,
limit=1000,
before=ctx.message,
delete_pinned=delete_pinned,
)
# Selfbot convenience, delete trigger message
if author == self.bot.user:
to_delete.append(ctx.message)
if channel.name:
channel_name = 'channel ' + channel.name
if ctx.guild:
channel_name = "channel " + channel.name
else:
channel_name = str(channel)
reason = "{}({}) deleted {} messages "\
"sent by the bot in {}."\
"".format(author.name, author.id, len(to_delete),
channel_name)
reason = (
"{}({}) deleted {} messages "
"sent by the bot in {}."
"".format(author.name, author.id, len(to_delete), channel_name)
)
log.info(reason)
if is_bot and can_mass_purge:

View File

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

View File

@@ -25,10 +25,9 @@ class AlreadyExists(CCError):
class CommandObj:
def __init__(self, **kwargs):
config = kwargs.get('config')
self.bot = kwargs.get('bot')
config = kwargs.get("config")
self.bot = kwargs.get("bot")
self.db = config.guild
@staticmethod
@@ -40,22 +39,23 @@ class CommandObj:
return customcommands
async def get_responses(self, ctx):
intro = (_("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 `{}`").format(
"customcommand", "customcommand", "exit()"
))
intro = _(
"Welcome to the interactive random {} maker!\n"
"Every message you send will be added as one of the random "
"responses to choose from once this {} is "
"triggered. To exit this interactive menu, type `{}`"
).format("customcommand", "customcommand", "exit()")
await ctx.send(intro)
def check(m):
return m.channel == ctx.channel and m.author == ctx.message.author
responses = []
while True:
await ctx.send(_("Add a random response:"))
msg = await self.bot.wait_for('message', check=check)
msg = await self.bot.wait_for("message", check=check)
if msg.content.lower() == 'exit()':
if msg.content.lower() == "exit()":
break
else:
responses.append(msg.content)
@@ -64,44 +64,31 @@ class CommandObj:
def get_now(self) -> str:
# Get current time as a string, for 'created_at' and 'edited_at' fields
# in the ccinfo dict
return '{:%d/%m/%Y %H:%M:%S}'.format(datetime.utcnow())
return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow())
async def get(self,
message: discord.Message,
command: str) -> str:
async def get(self, message: discord.Message, command: str) -> str:
ccinfo = await self.db(message.guild).commands.get_raw(command, default=None)
if not ccinfo:
raise NotFound
else:
return ccinfo['response']
return ccinfo["response"]
async def create(self,
ctx: commands.Context,
command: str,
response):
"""Create a customcommand"""
async def create(self, ctx: commands.Context, command: str, response):
"""Create a custom command"""
# Check if this command is already registered as a customcommand
if await self.db(ctx.guild).commands.get_raw(command, default=None):
raise AlreadyExists()
author = ctx.message.author
ccinfo = {
'author': {
'id': author.id,
'name': author.name
},
'command': command,
'created_at': self.get_now(),
'editors': [],
'response': response
"author": {"id": author.id, "name": author.name},
"command": command,
"created_at": self.get_now(),
"editors": [],
"response": response,
}
await self.db(ctx.guild).commands.set_raw(
command, value=ccinfo)
await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
async def edit(self,
ctx: commands.Context,
command: str,
response: None):
async def edit(self, ctx: commands.Context, command: str, response: None):
"""Edit an already existing custom command"""
# Check if this command is registered
if not await self.db(ctx.guild).commands.get_raw(command, default=None):
@@ -114,69 +101,55 @@ class CommandObj:
return m.channel == ctx.channel and m.author == ctx.message.author
if not response:
await ctx.send(
_("Do you want to create a 'randomized' cc? {}").format("y/n")
)
await ctx.send(_("Do you want to create a 'randomized' cc? {}").format("y/n"))
msg = await self.bot.wait_for('message', check=check)
if msg.content.lower() == 'y':
msg = await self.bot.wait_for("message", check=check)
if msg.content.lower() == "y":
response = await self.get_responses(ctx=ctx)
else:
await ctx.send(_("What response do you want?"))
response = (await self.bot.wait_for(
'message', check=check)
).content
response = (await self.bot.wait_for("message", check=check)).content
ccinfo['response'] = response
ccinfo['edited_at'] = self.get_now()
ccinfo["response"] = response
ccinfo["edited_at"] = self.get_now()
if author.id not in ccinfo['editors']:
if author.id not in ccinfo["editors"]:
# Add the person who invoked the `edit` coroutine to the list of
# editors, if the person is not yet in there
ccinfo['editors'].append(
author.id
)
ccinfo["editors"].append(author.id)
await self.db(ctx.guild).commands.set_raw(
command, value=ccinfo)
await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
async def delete(self,
ctx: commands.Context,
command: str):
async def delete(self, ctx: commands.Context, command: str):
"""Delete an already exisiting custom command"""
# Check if this command is registered
if not await self.db(ctx.guild).commands.get_raw(command, default=None):
raise NotFound()
await self.db(ctx.guild).commands.set_raw(
command, value=None)
await self.db(ctx.guild).commands.set_raw(command, value=None)
@cog_i18n(_)
class CustomCommands:
"""Custom commands
Creates commands used to display text"""
def __init__(self, bot):
self.bot = bot
self.key = 414589031223512
self.config = Config.get_conf(self,
self.key)
self.config = Config.get_conf(self, self.key)
self.config.register_guild(commands={})
self.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()
async def customcom(self,
ctx: commands.Context):
async def customcom(self, ctx: commands.Context):
"""Custom commands management"""
if not ctx.invoked_subcommand:
await ctx.send_help()
pass
@customcom.group(name="add")
@checks.mod_or_permissions(administrator=True)
async def cc_add(self,
ctx: commands.Context):
async def cc_add(self, ctx: commands.Context):
"""
CCs can be enhanced with arguments:
@@ -192,17 +165,14 @@ class CustomCommands:
{server} message.guild
"""
if not ctx.invoked_subcommand or isinstance(ctx.invoked_subcommand,
commands.Group):
await ctx.send_help()
pass
@cc_add.command(name='random')
@cc_add.command(name="random")
@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!
Note: This is interactive
"""
channel = ctx.channel
@@ -210,27 +180,22 @@ class CustomCommands:
responses = await self.commandobj.get_responses(ctx=ctx)
try:
await self.commandobj.create(ctx=ctx,
command=command,
response=responses)
await self.commandobj.create(ctx=ctx, command=command, response=responses)
await ctx.send(_("Custom command successfully added."))
except AlreadyExists:
await ctx.send(_(
"This command already exists. Use "
"`{}` to edit it.").format(
await ctx.send(
_("This command already exists. Use `{}` to edit it.").format(
"{}customcom edit".format(ctx.prefix)
))
)
)
# await ctx.send(str(responses))
@cc_add.command(name="simple")
@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
Example:
[p]customcom add simple yourcommand Text you want
"""
@@ -240,25 +205,20 @@ class CustomCommands:
await ctx.send(_("That command is already a standard command."))
return
try:
await self.commandobj.create(ctx=ctx,
command=command,
response=text)
await self.commandobj.create(ctx=ctx, command=command, response=text)
await ctx.send(_("Custom command successfully added."))
except AlreadyExists:
await ctx.send(_(
"This command already exists. Use "
"`{}` to edit it.").format(
await ctx.send(
_("This command already exists. Use `{}` to edit it.").format(
"{}customcom edit".format(ctx.prefix)
))
)
)
@customcom.command(name="edit")
@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
Example:
[p]customcom edit yourcommand Text you want
"""
@@ -266,61 +226,55 @@ class CustomCommands:
command = command.lower()
try:
await self.commandobj.edit(ctx=ctx,
command=command,
response=text)
await self.commandobj.edit(ctx=ctx, command=command, response=text)
await ctx.send(_("Custom command successfully edited."))
except NotFound:
await ctx.send(_(
"That command doesn't exist. Use "
"`{}` to add it.").format(
await ctx.send(
_("That command doesn't exist. Use `{}` to add it.").format(
"{}customcom add".format(ctx.prefix)
))
)
)
@customcom.command(name="delete")
@checks.mod_or_permissions(administrator=True)
async def cc_delete(self,
ctx,
command: str):
async def cc_delete(self, ctx, command: str):
"""Deletes a custom command
Example:
[p]customcom delete yourcommand"""
guild = ctx.message.guild
command = command.lower()
try:
await self.commandobj.delete(ctx=ctx,
command=command)
await self.commandobj.delete(ctx=ctx, command=command)
await ctx.send(_("Custom command successfully deleted."))
except NotFound:
await ctx.send(_("That command doesn't exist."))
@customcom.command(name="list")
async def cc_list(self,
ctx):
async def cc_list(self, ctx):
"""Shows custom commands list"""
response = await CommandObj.get_commands(self.config.guild(ctx.guild))
if not response:
await ctx.send(_(
"There are no custom commands in this server."
" Use `{}` to start adding some.").format(
"{}customcom add".format(ctx.prefix)
))
await ctx.send(
_(
"There are no custom commands in this server."
" Use `{}` to start adding some."
).format("{}customcom add".format(ctx.prefix))
)
return
results = []
for command, body in response.items():
responses = body['response']
responses = body["response"]
if isinstance(responses, list):
result = ", ".join(responses)
elif isinstance(responses, str):
result = responses
else:
continue
results.append("{command:<15} : {result}".format(command=command,
result=result))
results.append("{command:<15} : {result}".format(command=command, result=result))
commands = "\n".join(results)
@@ -330,14 +284,13 @@ class CustomCommands:
for page in pagify(commands, delims=[" ", "\n"]):
await ctx.author.send(box(page))
async def on_message(self,
message):
async def on_message(self, message):
is_private = isinstance(message.channel, discord.abc.PrivateChannel)
if len(message.content) < 2 or is_private:
return
guild = message.guild
prefixes = await self.bot.db.guild(guild).get_raw('prefix', default=[])
prefixes = await self.bot.db.guild(guild).get_raw("prefix", default=[])
if len(prefixes) < 1:
def_prefixes = await self.bot.get_prefix(message)
@@ -356,10 +309,9 @@ class CustomCommands:
return
if user_allowed:
cmd = message.content[len(prefix):]
cmd = message.content[len(prefix) :]
try:
c = await self.commandobj.get(message=message,
command=cmd)
c = await self.commandobj.get(message=message, command=cmd)
if isinstance(c, list):
command = random.choice(c)
elif isinstance(c, str):
@@ -371,18 +323,14 @@ class CustomCommands:
response = self.format_cc(command, message)
await message.channel.send(response)
def format_cc(self,
command,
message) -> str:
def format_cc(self, command, message) -> str:
results = re.findall("\{([^}]+)\}", command)
for result in results:
param = self.transform_parameter(result, message)
command = command.replace("{" + result + "}", param)
return command
def transform_parameter(self,
result,
message) -> str:
def transform_parameter(self, result, message) -> str:
"""
For security reasons only specific objects are allowed
Internals are ignored
@@ -393,7 +341,7 @@ class CustomCommands:
"author": message.author,
"channel": message.channel,
"guild": message.guild,
"server": message.guild
"server": message.guild,
}
if result in objects:
return str(objects[result])

View File

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

View File

@@ -16,49 +16,49 @@ class SpecResolver(object):
self.v2path = path
self.resolved = set()
self.available_core_conversions = {
'Bank Accounts': {
'cfg': ('Bank', None, 384734293238749),
'file': self.v2path / 'data' / 'economy' / 'bank.json',
'converter': self.bank_accounts_conv_spec
"Bank Accounts": {
"cfg": ("Bank", None, 384734293238749),
"file": self.v2path / "data" / "economy" / "bank.json",
"converter": self.bank_accounts_conv_spec,
},
'Economy Settings': {
'cfg': ('Economy', 'config', 1256844281),
'file': self.v2path / 'data' / 'economy' / 'settings.json',
'converter': self.economy_conv_spec
"Economy Settings": {
"cfg": ("Economy", "config", 1256844281),
"file": self.v2path / "data" / "economy" / "settings.json",
"converter": self.economy_conv_spec,
},
'Mod Log Cases': {
'cfg': ('ModLog', None, 1354799444),
'file': self.v2path / 'data' / 'mod' / 'modlog.json',
'converter': None # prevents from showing as available
"Mod Log Cases": {
"cfg": ("ModLog", None, 1354799444),
"file": self.v2path / "data" / "mod" / "modlog.json",
"converter": None, # prevents from showing as available
},
'Filter': {
'cfg': ('Filter', 'settings', 4766951341),
'file': self.v2path / 'data' / 'mod' / 'filter.json',
'converter': self.filter_conv_spec
"Filter": {
"cfg": ("Filter", "settings", 4766951341),
"file": self.v2path / "data" / "mod" / "filter.json",
"converter": self.filter_conv_spec,
},
'Past Names': {
'cfg': ('Mod', 'settings', 4961522000),
'file': self.v2path / 'data' / 'mod' / 'past_names.json',
'converter': self.past_names_conv_spec
"Past Names": {
"cfg": ("Mod", "settings", 4961522000),
"file": self.v2path / "data" / "mod" / "past_names.json",
"converter": self.past_names_conv_spec,
},
'Past Nicknames': {
'cfg': ('Mod', 'settings', 4961522000),
'file': self.v2path / 'data' / 'mod' / 'past_nicknames.json',
'converter': self.past_nicknames_conv_spec
"Past Nicknames": {
"cfg": ("Mod", "settings", 4961522000),
"file": self.v2path / "data" / "mod" / "past_nicknames.json",
"converter": self.past_nicknames_conv_spec,
},
"Custom Commands": {
"cfg": ("CustomCommands", "config", 414589031223512),
"file": self.v2path / "data" / "customcom" / "commands.json",
"converter": self.customcom_conv_spec,
},
'Custom Commands': {
'cfg': ('CustomCommands', 'config', 414589031223512),
'file': self.v2path / 'data' / 'customcom' / 'commands.json',
'converter': self.customcom_conv_spec
}
}
@property
def available(self):
return sorted(
k for k, v in self.available_core_conversions.items()
if v['file'].is_file() and v['converter'] is not None
and k not in self.resolved
k
for k, v in self.available_core_conversions.items()
if v["file"].is_file() and v["converter"] is not None and k not in self.resolved
)
def unpack(self, parent_key, parent_value):
@@ -75,15 +75,8 @@ class SpecResolver(object):
"""Flatten a nested dictionary structure"""
dictionary = {(key,): value for key, value in dictionary.items()}
while True:
dictionary = dict(
chain.from_iterable(
starmap(self.unpack, dictionary.items())
)
)
if not any(
isinstance(value, dict)
for value in dictionary.values()
):
dictionary = dict(chain.from_iterable(starmap(self.unpack, dictionary.items())))
if not any(isinstance(value, dict) for value in dictionary.values()):
break
return dictionary
@@ -97,11 +90,8 @@ class SpecResolver(object):
outerkey, innerkey = tuple(k[:-1]), (k[-1],)
if outerkey not in ret:
ret[outerkey] = {}
if innerkey[0] == 'created_at':
x = int(
datetime.strptime(
v, "%Y-%m-%d %H:%M:%S").timestamp()
)
if innerkey[0] == "created_at":
x = int(datetime.strptime(v, "%Y-%m-%d %H:%M:%S").timestamp())
ret[outerkey].update({innerkey: x})
else:
ret[outerkey].update({innerkey: v})
@@ -121,60 +111,60 @@ class SpecResolver(object):
raise NotImplementedError("This one isn't ready yet")
def filter_conv_spec(self, data: dict):
return {
(Config.GUILD, k): {('filter',): v}
for k, v in data.items()
}
return {(Config.GUILD, k): {("filter",): v} for k, v in data.items()}
def past_names_conv_spec(self, data: dict):
return {
(Config.USER, k): {('past_names',): v}
for k, v in data.items()
}
return {(Config.USER, k): {("past_names",): v} for k, v in data.items()}
def past_nicknames_conv_spec(self, data: dict):
flatscoped = self.apply_scope(Config.MEMBER, self.flatten_dict(data))
ret = {}
for k, v in flatscoped.items():
outerkey, innerkey = (*k[:-1],), (k[-1],)
if outerkey not in ret:
ret[outerkey] = {}
ret[outerkey].update({innerkey: v})
for config_identifiers, v2data in flatscoped.items():
if config_identifiers not in ret:
ret[config_identifiers] = {}
ret[config_identifiers].update({("past_nicks",): v2data})
return ret
def customcom_conv_spec(self, data: dict):
flatscoped = self.apply_scope(Config.GUILD, self.flatten_dict(data))
ret = {}
for k, v in flatscoped.items():
outerkey, innerkey = (*k[:-1],), ('commands', k[-1])
outerkey, innerkey = (*k[:-1],), ("commands", k[-1])
if outerkey not in ret:
ret[outerkey] = {}
ccinfo = {
'author': {
'id': 42,
'name': 'Converted from a v2 instance'
},
'command': k[-1],
'created_at': '{:%d/%m/%Y %H:%M:%S}'.format(datetime.utcnow()),
'editors': [],
'response': v
"author": {"id": 42, "name": "Converted from a v2 instance"},
"command": k[-1],
"created_at": "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow()),
"editors": [],
"response": v,
}
ret[outerkey].update({innerkey: ccinfo})
return ret
async def convert(self, bot: Red, prettyname: str):
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']
def get_config_object(self, bot, cogname, attr, _id):
try:
config = getattr(bot.get_cog(cogname), attr)
except (TypeError, AttributeError):
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:
items = converter(dc.json_load(filepath))
await dc(config).dict_import(items)

View File

@@ -7,7 +7,7 @@ from redbot.core.i18n import Translator, cog_i18n
from redbot.cogs.dataconverter.core_specs import SpecResolver
from redbot.core.utils.chat_formatting import box
_ = Translator('DataConverter', __file__)
_ = Translator("DataConverter", __file__)
@cog_i18n(_)
@@ -34,13 +34,14 @@ class DataConverter:
if not resolver.available:
return await ctx.send(
_("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?")
_(
"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?"
)
)
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):
menu += "\n{}. {}".format(index, entry)
@@ -50,24 +51,17 @@ class DataConverter:
return m.channel == ctx.channel and m.author == ctx.author
try:
message = await self.bot.wait_for(
'message', check=pred, timeout=60
)
message = await self.bot.wait_for("message", check=pred, timeout=60)
except asyncio.TimeoutError:
return await ctx.send(
_('Try this again when you are more ready'))
return await ctx.send(_("Try this again when you are more ready"))
else:
if message.content.strip().lower() in [
'quit', 'exit', '-1', 'q', 'cancel'
]:
if message.content.strip().lower() in ["quit", "exit", "-1", "q", "cancel"]:
return await ctx.tick()
try:
message = int(message.content.strip())
to_conv = resolver.available[message - 1]
except (ValueError, IndexError):
await ctx.send(
_("That wasn't a valid choice.")
)
await ctx.send(_("That wasn't a valid choice."))
continue
else:
async with ctx.typing():
@@ -76,6 +70,8 @@ class DataConverter:
await menu_message.delete()
else:
return await ctx.send(
_("There isn't anything else I know how to convert here."
"\nThere might be more things I can convert in the future.")
_(
"There isn't anything else I know how to convert here."
"\nThere might be more things I can convert in the future."
)
)

View File

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

View File

@@ -1,9 +1,9 @@
import asyncio
import discord
from discord.ext import commands
from redbot.core import commands
__all__ = ["install_agreement", ]
__all__ = ["do_install_agreement"]
REPO_INSTALL_MSG = (
"You're about to add a 3rd party repository. The creator of Red"
@@ -16,30 +16,21 @@ REPO_INSTALL_MSG = (
)
def install_agreement():
async def pred(ctx: commands.Context):
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
def does_agree(msg: discord.Message):
return ctx.author == msg.author and \
ctx.channel == msg.channel and \
msg.content == "I agree"
await ctx.send(REPO_INSTALL_MSG)
try:
await ctx.bot.wait_for('message', check=does_agree, timeout=30)
except asyncio.TimeoutError:
await ctx.send("Your response has timed out, please try again.")
return False
downloader.already_agreed = True
async def do_install_agreement(ctx: commands.Context):
downloader = ctx.cog
if downloader is None or downloader.already_agreed:
return True
return commands.check(pred)
def does_agree(msg: discord.Message):
return ctx.author == msg.author and ctx.channel == msg.channel and msg.content == "I agree"
await ctx.send(REPO_INSTALL_MSG)
try:
await ctx.bot.wait_for("message", check=does_agree, timeout=30)
except asyncio.TimeoutError:
await ctx.send("Your response has timed out, please try again.")
return False
downloader.already_agreed = True
return True

View File

@@ -1,6 +1,5 @@
import discord
from discord.ext import commands
from .repo_manager import RepoManager
from redbot.core import commands
from .installable import Installable
@@ -12,8 +11,6 @@ class InstalledCog(commands.Converter):
cog = discord.utils.get(await downloader.installed_cogs(), name=arg)
if cog is None:
raise commands.BadArgument(
"That cog is not installed"
)
raise commands.BadArgument("That cog is not installed")
return cog

View File

@@ -15,14 +15,14 @@ from redbot.core.utils.chat_formatting import box, pagify
from redbot.core import commands
from redbot.core.bot import Red
from .checks import install_agreement
from .checks import do_install_agreement
from .converters import InstalledCog
from .errors import CloningError, ExistingGitRepo
from .installable import Installable
from .log import log
from .repo_manager import RepoManager, Repo
_ = Translator('Downloader', __file__)
_ = Translator("Downloader", __file__)
@cog_i18n(_)
@@ -30,12 +30,9 @@ class Downloader:
def __init__(self, bot: Red):
self.bot = bot
self.conf = Config.get_conf(self, identifier=998240343,
force_registration=True)
self.conf = Config.get_conf(self, identifier=998240343, force_registration=True)
self.conf.register_global(
installed=[]
)
self.conf.register_global(installed=[])
self.already_agreed = False
@@ -46,17 +43,17 @@ class Downloader:
self.LIB_PATH.mkdir(parents=True, exist_ok=True)
self.SHAREDLIB_PATH.mkdir(parents=True, exist_ok=True)
if not self.SHAREDLIB_INIT.exists():
with self.SHAREDLIB_INIT.open(mode='w', encoding='utf-8') as _:
with self.SHAREDLIB_INIT.open(mode="w", encoding="utf-8") as _:
pass
if str(self.LIB_PATH) not in syspath:
syspath.insert(1, str(self.LIB_PATH))
self._repo_manager = RepoManager(self.conf)
self._repo_manager = RepoManager()
async def cog_install_path(self):
"""Get the current cog install path.
Returns
-------
pathlib.Path
@@ -67,7 +64,7 @@ class Downloader:
async def installed_cogs(self) -> Tuple[Installable]:
"""Get info on installed cogs.
Returns
-------
`tuple` of `Installable`
@@ -80,7 +77,7 @@ class Downloader:
async def _add_to_installed(self, cog: Installable):
"""Mark a cog as installed.
Parameters
----------
cog : Installable
@@ -96,7 +93,7 @@ class Downloader:
async def _remove_from_installed(self, cog: Installable):
"""Remove a cog from the saved list of installed cogs.
Parameters
----------
cog : Installable
@@ -170,7 +167,7 @@ class Downloader:
for repo, reqs in has_reqs:
for req in reqs:
# noinspection PyTypeChecker
ret = ret and await repo.install_raw_requirements([req, ], self.LIB_PATH)
ret = ret and await repo.install_raw_requirements([req], self.LIB_PATH)
return ret
@staticmethod
@@ -200,8 +197,12 @@ class Downloader:
if success:
await ctx.send(_("Libraries installed."))
else:
await ctx.send(_("Some libraries failed to install. Please check"
" your logs for a complete list."))
await ctx.send(
_(
"Some libraries failed to install. Please check"
" your logs for a complete list."
)
)
@commands.group()
@checks.is_owner()
@@ -209,25 +210,22 @@ class Downloader:
"""
Command group for managing Downloader repos.
"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
pass
@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.
Name can only contain characters A-z, numbers and underscore
Branch will default to master if not specified
"""
agreed = await do_install_agreement(ctx)
if not agreed:
return
try:
# noinspection PyTypeChecker
repo = await self._repo_manager.add_repo(
name=name,
url=repo_url,
branch=branch
)
repo = await self._repo_manager.add_repo(name=name, url=repo_url, branch=branch)
except ExistingGitRepo:
await ctx.send(_("That git repo has already been added under another name."))
except CloningError:
@@ -236,7 +234,7 @@ class Downloader:
else:
await ctx.send(_("Repo `{}` successfully added.").format(name))
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")
async def _repo_del(self, ctx, repo_name: Repo):
@@ -254,19 +252,33 @@ class Downloader:
"""
repos = self._repo_manager.get_all_repo_names()
repos = sorted(repos, key=str.lower)
joined = _("Installed Repos:\n") + "\n".join(["+ " + r for r in repos])
joined = _("Installed Repos:\n\n")
for repo_name in repos:
repo = self._repo_manager.get_repo(repo_name)
joined += "+ {}: {}\n".format(repo.name, repo.short or "")
for page in pagify(joined, ["\n"], shorten_by=16):
await ctx.send(box(page.lstrip(" "), lang="diff"))
@repo.command(name="info")
async def _repo_info(self, ctx, repo_name: Repo):
"""
Lists information about a single repo
"""
if repo_name is None:
await ctx.send(_("There is no repo `{}`").format(repo_name.name))
return
msg = _("Information on {}:\n{}").format(repo_name.name, repo_name.description or "")
await ctx.send(box(msg))
@commands.group()
@checks.is_owner()
async def cog(self, ctx):
"""
Command group for managing installable Cogs.
"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
pass
@cog.command(name="install")
async def _cog_install(self, ctx, repo_name: Repo, cog_name: str):
@@ -275,20 +287,28 @@ class Downloader:
"""
cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable
if cog is None:
await ctx.send(_("Error, there is no cog by the name of"
" `{}` in the `{}` repo.").format(cog_name, repo_name.name))
await ctx.send(
_("Error, there is no cog by the name of `{}` in the `{}` repo.").format(
cog_name, repo_name.name
)
)
return
elif cog.min_python_version > sys.version_info:
await ctx.send(_(
"This cog requires at least python version {}, aborting install.".format(
'.'.join([str(n) for n in cog.min_python_version])
await ctx.send(
_(
"This cog requires at least python version {}, aborting install.".format(
".".join([str(n) for n in cog.min_python_version])
)
)
))
)
return
if not await repo_name.install_requirements(cog, self.LIB_PATH):
await ctx.send(_("Failed to install the required libraries for"
" `{}`: `{}`").format(cog.name, cog.requirements))
await ctx.send(
_("Failed to install the required libraries for `{}`: `{}`").format(
cog.name, cog.requirements
)
)
return
await repo_name.install_cog(cog, await self.cog_install_path())
@@ -299,7 +319,7 @@ class Downloader:
await ctx.send(_("`{}` cog successfully installed.").format(cog_name))
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")
async def _cog_uninstall(self, ctx, cog_name: InstalledCog):
@@ -317,12 +337,16 @@ class Downloader:
await self._remove_from_installed(cog_name)
await ctx.send(_("`{}` was successfully removed.").format(real_name))
else:
await ctx.send(_("That cog was installed but can no longer"
" be located. You may need to remove it's"
" files manually if it is still usable."))
await ctx.send(
_(
"That cog was installed but can no longer"
" be located. You may need to remove it's"
" files manually if it is still usable."
)
)
@cog.command(name="update")
async def _cog_update(self, ctx, cog_name: InstalledCog=None):
async def _cog_update(self, ctx, cog_name: InstalledCog = None):
"""
Updates all cogs or one of your choosing.
"""
@@ -356,11 +380,27 @@ class Downloader:
"""
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 = _("Available Cogs:\n") + "\n".join(
["+ {}: {}".format(c.name, c.short or "") for c in cogs])
await ctx.send(box(cogs, lang="diff"))
[
"+ {}: {}".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):
await ctx.send(box(page.lstrip(" "), lang="diff"))
@cog.command(name="info")
async def _cog_info(self, ctx, repo_name: Repo, cog_name: str):
@@ -369,15 +409,19 @@ class Downloader:
"""
cog = discord.utils.get(repo_name.available_cogs, name=cog_name)
if cog is None:
await ctx.send(_("There is no cog `{}` in the repo `{}`").format(
cog_name, repo_name.name
))
await ctx.send(
_("There is no cog `{}` in the repo `{}`").format(cog_name, repo_name.name)
)
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))
async def is_installed(self, cog_name: str) -> (bool, Union[Installable, None]):
async def is_installed(
self, cog_name: str
) -> Union[Tuple[bool, Installable], Tuple[bool, None]]:
"""Check to see if a cog has been installed through Downloader.
Parameters
@@ -397,8 +441,9 @@ class Downloader:
return True, installable
return False, None
def format_findcog_info(self, command_name: str,
cog_installable: Union[Installable, object]=None) -> str:
def format_findcog_info(
self, command_name: str, cog_installable: Union[Installable, object] = None
) -> str:
"""Format a cog's info for output to discord.
Parameters
@@ -407,7 +452,7 @@ class Downloader:
Name of the command which belongs to the cog.
cog_installable : `Installable` or `object`
Can be an `Installable` instance or a Cog instance.
Returns
-------
str
@@ -432,19 +477,19 @@ class Downloader:
"""Determines the cog name that Downloader knows from the cog instance.
Probably.
Parameters
----------
instance : object
The cog instance.
Returns
-------
str
The name of the cog according to Downloader..
"""
splitted = instance.__module__.split('.')
splitted = instance.__module__.split(".")
return splitted[-2]
@commands.command()

View File

@@ -1,12 +1,23 @@
__all__ = ["DownloaderException", "GitException", "InvalidRepoName", "ExistingGitRepo",
"MissingGitRepo", "CloningError", "CurrentHashError", "HardResetError",
"UpdateError", "GitDiffError", "PipError"]
__all__ = [
"DownloaderException",
"GitException",
"InvalidRepoName",
"ExistingGitRepo",
"MissingGitRepo",
"CloningError",
"CurrentHashError",
"HardResetError",
"UpdateError",
"GitDiffError",
"PipError",
]
class DownloaderException(Exception):
"""
Base class for Downloader exceptions.
"""
pass
@@ -21,6 +32,7 @@ class InvalidRepoName(DownloaderException):
Throw when a repo name is invalid. Check
the message for a more detailed reason.
"""
pass
@@ -29,6 +41,7 @@ class ExistingGitRepo(DownloaderException):
Thrown when trying to clone into a folder where a
git repo already exists.
"""
pass
@@ -37,6 +50,7 @@ class MissingGitRepo(DownloaderException):
Thrown when a git repo is expected to exist but
does not.
"""
pass
@@ -44,6 +58,7 @@ class CloningError(GitException):
"""
Thrown when git clone returns a non zero exit code.
"""
pass
@@ -52,6 +67,7 @@ class CurrentHashError(GitException):
Thrown when git returns a non zero exit code attempting
to determine the current commit hash.
"""
pass
@@ -60,6 +76,7 @@ class HardResetError(GitException):
Thrown when there is an issue trying to execute a hard reset
(usually prior to a repo update).
"""
pass
@@ -67,6 +84,7 @@ class UpdateError(GitException):
"""
Thrown when git pull returns a non zero error code.
"""
pass
@@ -74,6 +92,7 @@ class GitDiffError(GitException):
"""
Thrown when a git diff fails.
"""
pass
@@ -81,4 +100,5 @@ class PipError(DownloaderException):
"""
Thrown when pip returns a non-zero return code.
"""
pass

View File

@@ -3,9 +3,8 @@ import distutils.dir_util
import shutil
from enum import Enum
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 .json_mixins import RepoJSONMixin
@@ -25,7 +24,7 @@ class Installable(RepoJSONMixin):
- Modules
- Repo Libraries
- Other stuff?
The attributes of this class will mostly come from the installation's
info.json.
@@ -56,6 +55,7 @@ class Installable(RepoJSONMixin):
:class:`InstallationType`.
"""
def __init__(self, location: Path):
"""Base installable initializer.
@@ -75,6 +75,7 @@ class Installable(RepoJSONMixin):
self.bot_version = (3, 0, 0)
self.min_python_version = (3, 5, 1)
self.hidden = False
self.disabled = False
self.required_cogs = {} # Cog name -> repo URL
self.requirements = ()
self.tags = ()
@@ -114,13 +115,9 @@ class Installable(RepoJSONMixin):
# noinspection PyBroadException
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:
log.exception("Error occurred when copying path:"
" {}".format(self._location))
log.exception("Error occurred when copying path: {}".format(self._location))
return False
return True
@@ -130,7 +127,7 @@ class Installable(RepoJSONMixin):
if self._info_file.exists():
self._process_info_file()
def _process_info_file(self, info_file_path: Path=None) -> MutableMapping[str, Any]:
def _process_info_file(self, info_file_path: Path = None) -> MutableMapping[str, Any]:
"""
Processes an information file. Loads dependencies among other
information into this object.
@@ -144,13 +141,12 @@ class Installable(RepoJSONMixin):
raise ValueError("No valid information file path was found.")
info = {}
with info_file_path.open(encoding='utf-8') as f:
with info_file_path.open(encoding="utf-8") as f:
try:
info = json.load(f)
except json.JSONDecodeError:
info = {}
log.exception("Invalid JSON information file at path:"
" {}".format(info_file_path))
log.exception("Invalid JSON information file at path: {}".format(info_file_path))
else:
self._info = info
@@ -167,7 +163,7 @@ class Installable(RepoJSONMixin):
self.bot_version = bot_version
try:
min_python_version = tuple(info.get('min_python_version', [3, 5, 1]))
min_python_version = tuple(info.get("min_python_version", [3, 5, 1]))
except ValueError:
min_python_version = self.min_python_version
self.min_python_version = min_python_version
@@ -178,6 +174,12 @@ class Installable(RepoJSONMixin):
hidden = False
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.requirements = info.get("requirements", ())
@@ -200,15 +202,12 @@ class Installable(RepoJSONMixin):
return info
def to_json(self):
return {
"repo_name": self.repo_name,
"cog_name": self.name
}
return {"repo_name": self.repo_name, "cog_name": self.name}
@classmethod
def from_json(cls, data: dict, repo_mgr: "RepoManager"):
repo_name = data['repo_name']
cog_name = data['cog_name']
repo_name = data["repo_name"]
cog_name = data["cog_name"]
repo = repo_mgr.get_repo(repo_name)
if repo is not None:

View File

@@ -24,7 +24,7 @@ class RepoJSONMixin:
return
try:
with self._info_file.open(encoding='utf-8') as f:
with self._info_file.open(encoding="utf-8") as f:
info = json.load(f)
except json.JSONDecodeError:
return
@@ -34,4 +34,4 @@ class RepoJSONMixin:
self.author = info.get("author")
self.install_msg = info.get("install_msg")
self.short = info.get("short")
self.description = info.get("description")
self.description = info.get("description")

View File

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

View File

@@ -1,3 +1,3 @@
import logging
log = logging.getLogger("red.downloader")
log = logging.getLogger("red.downloader")

View File

@@ -2,17 +2,13 @@ import asyncio
import functools
import os
import pkgutil
import shutil
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from subprocess import run as sp_run, PIPE
from sys import executable
from typing import Tuple, MutableMapping, Union
from discord.ext import commands
from redbot.core import Config
from redbot.core import data_manager
from redbot.core import data_manager, commands
from redbot.core.utils import safe_delete
from .errors import *
from .installable import Installable, InstallableType
@@ -27,16 +23,21 @@ class Repo(RepoJSONMixin):
GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}"
GIT_HARD_RESET = "git -C {path} reset --hard origin/{branch} -q"
GIT_PULL = "git -C {path} pull -q --ff-only"
GIT_DIFF_FILE_STATUS = ("git -C {path} diff --no-commit-id --name-status"
" {old_hash} {new_hash}")
GIT_LOG = ("git -C {path} log --relative-date --reverse {old_hash}.."
" {relative_file_path}")
GIT_DIFF_FILE_STATUS = "git -C {path} diff --no-commit-id --name-status {old_hash} {new_hash}"
GIT_LOG = "git -C {path} log --relative-date --reverse {old_hash}.. {relative_file_path}"
GIT_DISCOVER_REMOTE_URL = "git -C {path} config --get remote.origin.url"
PIP_INSTALL = "{python} -m pip install -U -t {target_dir} {reqs}"
def __init__(self, name: str, url: str, branch: str, folder_path: Path,
available_modules: Tuple[Installable]=(), loop: asyncio.AbstractEventLoop=None):
def __init__(
self,
name: str,
url: str,
branch: str,
folder_path: Path,
available_modules: Tuple[Installable] = (),
loop: asyncio.AbstractEventLoop = None,
):
self.url = url
self.branch = branch
@@ -71,11 +72,12 @@ class Repo(RepoJSONMixin):
return poss_repo
def _existing_git_repo(self) -> (bool, Path):
git_path = self.folder_path / '.git'
git_path = self.folder_path / ".git"
return git_path.exists(), git_path
async def _get_file_update_statuses(
self, old_hash: str, new_hash: str) -> MutableMapping[str, str]:
self, old_hash: str, new_hash: str
) -> MutableMapping[str, str]:
"""
Gets the file update status letters for each changed file between
the two hashes.
@@ -85,29 +87,25 @@ class Repo(RepoJSONMixin):
"""
p = await self._run(
self.GIT_DIFF_FILE_STATUS.format(
path=self.folder_path,
old_hash=old_hash,
new_hash=new_hash
path=self.folder_path, old_hash=old_hash, new_hash=new_hash
)
)
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")
ret = {}
for filename in stdout:
# TODO: filter these filenames by ones in self.available_modules
status, _, filepath = filename.partition('\t')
status, _, filepath = filename.partition("\t")
ret[filepath] = status
return ret
async def _get_commit_notes(self, old_commit_hash: str,
relative_file_path: str) -> str:
async def _get_commit_notes(self, old_commit_hash: str, relative_file_path: str) -> str:
"""
Gets the commit notes from git log.
:param old_commit_hash: Point in time to start getting messages
@@ -119,13 +117,15 @@ class Repo(RepoJSONMixin):
self.GIT_LOG.format(
path=self.folder_path,
old_hash=old_commit_hash,
relative_file_path=relative_file_path
relative_file_path=relative_file_path,
)
)
if p.returncode != 0:
raise GitException("An exception occurred while executing git log on"
" this repo: {}".format(self.folder_path))
raise GitException(
"An exception occurred while executing git log on"
" this repo: {}".format(self.folder_path)
)
return p.stdout.decode().strip()
@@ -146,10 +146,11 @@ class Repo(RepoJSONMixin):
Installable(location=name)
)
"""
for file_finder, name, is_pkg in pkgutil.walk_packages(path=[str(self.folder_path), ]):
curr_modules.append(
Installable(location=self.folder_path / name)
)
for file_finder, name, is_pkg in pkgutil.walk_packages(
path=[str(self.folder_path)], onerror=lambda name: None
):
if is_pkg:
curr_modules.append(Installable(location=self.folder_path / name))
self.available_modules = curr_modules
# noinspection PyTypeChecker
@@ -157,12 +158,11 @@ class Repo(RepoJSONMixin):
async def _run(self, *args, **kwargs):
env = os.environ.copy()
env['GIT_TERMINAL_PROMPT'] = '0'
kwargs['env'] = env
env["GIT_TERMINAL_PROMPT"] = "0"
kwargs["env"] = env
async with self._repo_lock:
return await self._loop.run_in_executor(
self._executor,
functools.partial(sp_run, *args, stdout=PIPE, **kwargs)
self._executor, functools.partial(sp_run, *args, stdout=PIPE, **kwargs)
)
async def clone(self) -> Tuple[str]:
@@ -176,24 +176,17 @@ class Repo(RepoJSONMixin):
"""
exists, path = self._existing_git_repo()
if exists:
raise ExistingGitRepo(
"A git repo already exists at path: {}".format(path)
)
raise ExistingGitRepo("A git repo already exists at path: {}".format(path))
if self.branch is not None:
p = await self._run(
self.GIT_CLONE.format(
branch=self.branch,
url=self.url,
folder=self.folder_path
branch=self.branch, url=self.url, folder=self.folder_path
).split()
)
else:
p = await self._run(
self.GIT_CLONE_NO_BRANCH.format(
url=self.url,
folder=self.folder_path
).split()
self.GIT_CLONE_NO_BRANCH.format(url=self.url, folder=self.folder_path).split()
)
if p.returncode != 0:
@@ -217,30 +210,25 @@ class Repo(RepoJSONMixin):
"""
exists, _ = self._existing_git_repo()
if not exists:
raise MissingGitRepo(
"A git repo does not exist at path: {}".format(self.folder_path)
)
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
p = await self._run(
self.GIT_CURRENT_BRANCH.format(
path=self.folder_path
).split()
)
p = await self._run(self.GIT_CURRENT_BRANCH.format(path=self.folder_path).split())
if p.returncode != 0:
raise GitException("Could not determine current branch"
" at path: {}".format(self.folder_path))
raise GitException(
"Could not determine current branch at path: {}".format(self.folder_path)
)
return p.stdout.decode().strip()
async def current_commit(self, branch: str=None) -> str:
async def current_commit(self, branch: str = None) -> str:
"""Determine the current commit hash of the repo.
Parameters
----------
branch : `str`, optional
Override for repo's branch attribute.
Returns
-------
str
@@ -252,15 +240,10 @@ class Repo(RepoJSONMixin):
exists, _ = self._existing_git_repo()
if not exists:
raise MissingGitRepo(
"A git repo does not exist at path: {}".format(self.folder_path)
)
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
p = await self._run(
self.GIT_LATEST_COMMIT.format(
path=self.folder_path,
branch=branch
).split()
self.GIT_LATEST_COMMIT.format(path=self.folder_path, branch=branch).split()
)
if p.returncode != 0:
@@ -268,7 +251,7 @@ class Repo(RepoJSONMixin):
return p.stdout.decode().strip()
async def current_url(self, folder: Path=None) -> str:
async def current_url(self, folder: Path = None) -> str:
"""
Discovers the FETCH URL for a Git repo.
@@ -290,18 +273,14 @@ class Repo(RepoJSONMixin):
if folder is None:
folder = self.folder_path
p = await self._run(
Repo.GIT_DISCOVER_REMOTE_URL.format(
path=folder
).split()
)
p = await self._run(Repo.GIT_DISCOVER_REMOTE_URL.format(path=folder).split())
if p.returncode != 0:
raise RuntimeError("Unable to discover a repo URL.")
return p.stdout.decode().strip()
async def hard_reset(self, branch: str=None) -> None:
async def hard_reset(self, branch: str = None) -> None:
"""Perform a hard reset on the current repo.
Parameters
@@ -315,21 +294,18 @@ class Repo(RepoJSONMixin):
exists, _ = self._existing_git_repo()
if not exists:
raise MissingGitRepo(
"A git repo does not exist at path: {}".format(self.folder_path)
)
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
p = await self._run(
self.GIT_HARD_RESET.format(
path=self.folder_path,
branch=branch
).split()
self.GIT_HARD_RESET.format(path=self.folder_path, branch=branch).split()
)
if p.returncode != 0:
raise HardResetError("Some error occurred when trying to"
" execute a hard reset on the repo at"
" the following path: {}".format(self.folder_path))
raise HardResetError(
"Some error occurred when trying to"
" execute a hard reset on the repo at"
" the following path: {}".format(self.folder_path)
)
async def update(self) -> (str, str):
"""Update the current branch of this repo.
@@ -345,15 +321,13 @@ class Repo(RepoJSONMixin):
await self.hard_reset(branch=curr_branch)
p = await self._run(
self.GIT_PULL.format(
path=self.folder_path
).split()
)
p = await self._run(self.GIT_PULL.format(path=self.folder_path).split())
if p.returncode != 0:
raise UpdateError("Git pull returned a non zero exit code"
" for the repo located at path: {}".format(self.folder_path))
raise UpdateError(
"Git pull returned a non zero exit code"
" for the repo located at path: {}".format(self.folder_path)
)
new_commit = await self.current_commit(branch=curr_branch)
@@ -389,7 +363,9 @@ class Repo(RepoJSONMixin):
return await cog.copy_to(target_dir=target_dir)
async def install_libraries(self, target_dir: Path, libraries: Tuple[Installable]=()) -> bool:
async def install_libraries(
self, target_dir: Path, libraries: Tuple[Installable] = ()
) -> bool:
"""Install shared libraries to the target directory.
If :code:`libraries` is not specified, all shared libraries in the repo
@@ -401,7 +377,7 @@ class Repo(RepoJSONMixin):
Directory to install shared libraries to.
libraries : `tuple` of `Installable`
A subset of available libraries.
Returns
-------
bool
@@ -423,7 +399,7 @@ class Repo(RepoJSONMixin):
async def install_requirements(self, cog: Installable, target_dir: Path) -> bool:
"""Install a cog's requirements.
Requirements will be installed via pip directly into
:code:`target_dir`.
@@ -469,29 +445,28 @@ class Repo(RepoJSONMixin):
p = await self._run(
self.PIP_INSTALL.format(
python=executable,
target_dir=target_dir,
reqs=" ".join(requirements)
python=executable, target_dir=target_dir, reqs=" ".join(requirements)
).split()
)
if p.returncode != 0:
log.error("Something went wrong when installing"
" the following requirements:"
" {}".format(", ".join(requirements)))
log.error(
"Something went wrong when installing"
" the following requirements:"
" {}".format(", ".join(requirements))
)
return False
return True
@property
def available_cogs(self) -> Tuple[Installable]:
"""`tuple` of `installable` : All available cogs in this Repo.
This excludes hidden or shared packages.
"""
# noinspection PyTypeChecker
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
@@ -501,8 +476,7 @@ class Repo(RepoJSONMixin):
"""
# noinspection PyTypeChecker
return tuple(
[m for m in self.available_modules
if m.type == InstallableType.SHARED_LIBRARY]
[m for m in self.available_modules if m.type == InstallableType.SHARED_LIBRARY]
)
@classmethod
@@ -515,8 +489,7 @@ class Repo(RepoJSONMixin):
class RepoManager:
def __init__(self, downloader_config: Config):
self.downloader_config = downloader_config
def __init__(self):
self._repos = {}
@@ -526,7 +499,7 @@ class RepoManager:
@property
def repos_folder(self) -> Path:
data_folder = data_manager.cog_data_path(self)
return data_folder / 'repos'
return data_folder / "repos"
def does_repo_exist(self, name: str) -> bool:
return name in self._repos
@@ -537,7 +510,7 @@ class RepoManager:
raise InvalidRepoName("Not a valid Python variable name.")
return name.lower()
async def add_repo(self, url: str, name: str, branch: str="master") -> Repo:
async def add_repo(self, url: str, name: str, branch: str = "master") -> Repo:
"""Add and clone a git repository.
Parameters
@@ -556,14 +529,12 @@ class RepoManager:
"""
if self.does_repo_exist(name):
raise InvalidRepoName(
"That repo name you provided already exists."
" Please choose another."
raise ExistingGitRepo(
"That repo name you provided already exists. Please choose another."
)
# noinspection PyTypeChecker
r = Repo(url=url, name=name, branch=branch,
folder_path=self.repos_folder / name)
r = Repo(url=url, name=name, branch=branch, folder_path=self.repos_folder / name)
await r.clone()
self._repos[name] = r

View File

@@ -9,7 +9,8 @@ import discord
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
from redbot.core import Config, bank, commands
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
@@ -36,42 +37,41 @@ class SMReel(Enum):
PAYOUTS = {
(SMReel.two, SMReel.two, SMReel.six): {
"payout": lambda x: x * 2500 + x,
"phrase": _("JACKPOT! 226! Your bid has been multiplied * 2500!")
"phrase": _("JACKPOT! 226! Your bid has been multiplied * 2500!"),
},
(SMReel.flc, SMReel.flc, SMReel.flc): {
"payout": lambda x: x + 1000,
"phrase": _("4LC! +1000!")
"phrase": _("4LC! +1000!"),
},
(SMReel.cherries, SMReel.cherries, SMReel.cherries): {
"payout": lambda x: x + 800,
"phrase": _("Three cherries! +800!")
"phrase": _("Three cherries! +800!"),
},
(SMReel.two, SMReel.six): {
"payout": lambda x: x * 4 + x,
"phrase": _("2 6! Your bid has been multiplied * 4!")
"phrase": _("2 6! Your bid has been multiplied * 4!"),
},
(SMReel.cherries, SMReel.cherries): {
"payout": lambda x: x * 3 + x,
"phrase": _("Two cherries! Your bid has been multiplied * 3!")
},
"3 symbols": {
"payout": lambda x: x + 500,
"phrase": _("Three symbols! +500!")
"phrase": _("Two cherries! Your bid has been multiplied * 3!"),
},
"3 symbols": {"payout": lambda x: x + 500, "phrase": _("Three symbols! +500!")},
"2 symbols": {
"payout": lambda x: x * 2 + x,
"phrase": _("Two consecutive symbols! Your bid has been multiplied * 2!")
"phrase": _("Two consecutive symbols! Your bid has been multiplied * 2!"),
},
}
SLOT_PAYOUTS_MSG = _("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").format(**SMReel.__dict__)
SLOT_PAYOUTS_MSG = _(
"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"
).format(**SMReel.__dict__)
def guild_only_check():
@@ -82,6 +82,7 @@ def guild_only_check():
return True
else:
return False
return commands.check(pred)
@@ -115,19 +116,14 @@ class Economy:
"SLOT_MIN": 5,
"SLOT_MAX": 100,
"SLOT_TIME": 0,
"REGISTER_CREDITS": 0
"REGISTER_CREDITS": 0,
}
default_global_settings = default_guild_settings
default_member_settings = {
"next_payday": 0,
"last_slot": 0
}
default_member_settings = {"next_payday": 0, "last_slot": 0}
default_role_settings = {
"PAYDAY_CREDITS": 0
}
default_role_settings = {"PAYDAY_CREDITS": 0}
default_user_settings = default_member_settings
@@ -142,11 +138,11 @@ class Economy:
self.config.register_role(**self.default_role_settings)
self.slot_register = defaultdict(dict)
@guild_only_check()
@commands.group(name="bank")
async def _bank(self, ctx: commands.Context):
"""Bank operations"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
pass
@_bank.command()
async def balance(self, ctx: commands.Context, user: discord.Member = None):
@@ -159,8 +155,7 @@ class Economy:
bal = await bank.get_balance(user)
currency = await bank.get_currency_name(ctx.guild)
await ctx.send(_("{}'s balance is {} {}").format(
user.display_name, bal, currency))
await ctx.send(_("{}'s balance is {} {}").format(user.display_name, bal, currency))
@_bank.command()
async def transfer(self, ctx: commands.Context, to: discord.Member, amount: int):
@@ -171,11 +166,13 @@ class Economy:
try:
await bank.transfer_credits(from_, to, amount)
except ValueError as e:
await ctx.send(str(e))
return await ctx.send(str(e))
await ctx.send(_("{} transferred {} {} to {}").format(
from_.display_name, amount, currency, to.display_name
))
await ctx.send(
_("{} transferred {} {} to {}").format(
from_.display_name, amount, currency, to.display_name
)
)
@_bank.command(name="set")
@check_global_setting_admin()
@@ -193,43 +190,49 @@ class Economy:
if creds.operation == "deposit":
await bank.deposit_credits(to, creds.sum)
await ctx.send(_("{} added {} {} to {}'s account.").format(
author.display_name, creds.sum, currency, to.display_name
))
await ctx.send(
_("{} added {} {} to {}'s account.").format(
author.display_name, creds.sum, currency, to.display_name
)
)
elif creds.operation == "withdraw":
await bank.withdraw_credits(to, creds.sum)
await ctx.send(_("{} removed {} {} from {}'s account.").format(
author.display_name, creds.sum, currency, to.display_name
))
await ctx.send(
_("{} removed {} {} from {}'s account.").format(
author.display_name, creds.sum, currency, to.display_name
)
)
else:
await bank.set_balance(to, creds.sum)
await ctx.send(_("{} set {}'s account to {} {}.").format(
author.display_name, to.display_name, creds.sum, currency
))
await ctx.send(
_("{} set {}'s account to {} {}.").format(
author.display_name, to.display_name, creds.sum, currency
)
)
@_bank.command()
@guild_only_check()
@check_global_setting_guildowner()
async def reset(self, ctx, confirmation: bool = False):
"""Deletes bank accounts"""
if confirmation is False:
await ctx.send(
_("This will delete all bank accounts for {}.\nIf you're sure, type "
"`{}bank reset yes`").format(
self.bot.user.name if await bank.is_global() else "this server",
ctx.prefix
_(
"This will delete all bank accounts for {}.\nIf you're sure, type "
"`{}bank reset yes`"
).format(
self.bot.user.name if await bank.is_global() else "this server", ctx.prefix
)
)
else:
await bank.wipe_bank()
await ctx.send(_("All bank accounts for {} have been "
"deleted.").format(
self.bot.user.name if await bank.is_global() else "this server"
)
)
await ctx.send(
_("All bank accounts for {} have been deleted.").format(
self.bot.user.name if await bank.is_global() else "this server"
)
)
@commands.command()
@guild_only_check()
@commands.command()
async def payday(self, ctx: commands.Context):
"""Get some free currency"""
author = ctx.author
@@ -245,77 +248,98 @@ class Economy:
await self.config.user(author).next_payday.set(next_payday)
pos = await bank.get_leaderboard_position(author)
await ctx.send(_(
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
"You currently have {3} {1}.\n\n"
"You are currently #{4} on the leaderboard!"
).format(
author, credits_name, str(await self.config.PAYDAY_CREDITS()),
str(await bank.get_balance(author)), pos
))
await ctx.send(
_(
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
"You currently have {3} {1}.\n\n"
"You are currently #{4} on the global leaderboard!"
).format(
author,
credits_name,
str(await self.config.PAYDAY_CREDITS()),
str(await bank.get_balance(author)),
pos,
)
)
else:
dtime = self.display_time(next_payday - cur_time)
await ctx.send(
_("{} Too soon. For your next payday you have to"
" wait {}.").format(author.mention, dtime)
_("{} Too soon. For your next payday you have to wait {}.").format(
author.mention, dtime
)
)
else:
next_payday = await self.config.member(author).next_payday()
if cur_time >= next_payday:
credit_amount = await self.config.guild(guild).PAYDAY_CREDITS()
for role in author.roles:
role_credits = await self.config.role(role).PAYDAY_CREDITS() # Nice variable name
role_credits = await self.config.role(
role
).PAYDAY_CREDITS() # Nice variable name
if role_credits > credit_amount:
credit_amount = role_credits
await bank.deposit_credits(author, credit_amount)
next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME()
await self.config.member(author).next_payday.set(next_payday)
pos = await bank.get_leaderboard_position(author)
await ctx.send(_(
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
"You currently have {3} {1}.\n\n"
"You are currently #{4} on the leaderboard!"
).format(
author, credits_name, credit_amount,
str(await bank.get_balance(author)), pos
))
await ctx.send(
_(
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
"You currently have {3} {1}.\n\n"
"You are currently #{4} on the leaderboard!"
).format(
author,
credits_name,
credit_amount,
str(await bank.get_balance(author)),
pos,
)
)
else:
dtime = self.display_time(next_payday - cur_time)
await ctx.send(
_("{} Too soon. For your next payday you have to"
" wait {}.").format(author.mention, dtime))
_("{} Too soon. For your next payday you have to wait {}.").format(
author.mention, dtime
)
)
@commands.command()
@guild_only_check()
async def leaderboard(self, ctx: commands.Context, top: int = 10, show_global: bool=False):
async def leaderboard(self, ctx: commands.Context, top: int = 10, show_global: bool = False):
"""Prints out the leaderboard
Defaults to top 10"""
# Originally coded by Airenkun - edited by irdumb, rewritten by Palm__ for v3
guild = ctx.guild
author = ctx.author
if top < 1:
top = 10
if await bank.is_global() and show_global: # show_global is only applicable if bank is global
if (
await bank.is_global() and show_global
): # show_global is only applicable if bank is global
guild = None
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
if len(bank_sorted) < top:
top = len(bank_sorted)
highscore = ""
for pos, acc in enumerate(bank_sorted, 1):
pos = pos
poswidth = 2
name = acc[1]["name"]
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
header = f"{f'#':4}{f'Name':36}{f'Score':2}\n"
highscores = [
(
f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} "
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
)
if highscore != "":
for page in pagify(highscore, shorten_by=12):
await ctx.send(box(page, lang="py"))
if acc[0] != author.id
else (
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:
await ctx.send(_("There are no accounts in the bank."))
@@ -337,7 +361,11 @@ class Economy:
slot_time = await self.config.SLOT_TIME()
last_slot = await self.config.user(author).last_slot()
else:
valid_bid = await self.config.guild(guild).SLOT_MIN() <= bid <= await self.config.guild(guild).SLOT_MAX()
valid_bid = (
await self.config.guild(guild).SLOT_MIN()
<= bid
<= await self.config.guild(guild).SLOT_MAX()
)
slot_time = await self.config.guild(guild).SLOT_TIME()
last_slot = await self.config.member(author).last_slot()
now = calendar.timegm(ctx.message.created_at.utctimetuple())
@@ -364,9 +392,11 @@ class Economy:
default_reel.rotate(random.randint(-999, 999)) # weeeeee
new_reel = deque(default_reel, maxlen=3) # we need only 3 symbols
reels.append(new_reel) # for each reel
rows = ((reels[0][0], reels[1][0], reels[2][0]),
(reels[0][1], reels[1][1], reels[2][1]),
(reels[0][2], reels[1][2], reels[2][2]))
rows = (
(reels[0][0], reels[1][0], reels[2][0]),
(reels[0][1], reels[1][1], reels[2][1]),
(reels[0][2], reels[1][2], reels[2][2]),
)
slot = "~~\n~~" # Mobile friendly
for i, row in enumerate(rows): # Let's build the slot to show
@@ -378,8 +408,7 @@ class Economy:
payout = PAYOUTS.get(rows[1])
if not payout:
# Checks for two-consecutive-symbols special rewards
payout = PAYOUTS.get((rows[1][0], rows[1][1]),
PAYOUTS.get((rows[1][1], rows[1][2])))
payout = PAYOUTS.get((rows[1][0], rows[1][1]), PAYOUTS.get((rows[1][1], rows[1][2])))
if not payout:
# Still nothing. Let's check for 3 generic same symbols
# or 2 consecutive symbols
@@ -395,15 +424,20 @@ class Economy:
pay = payout["payout"](bid)
now = then - bid + pay
await bank.set_balance(author, now)
await channel.send(_("{}\n{} {}\n\nYour bid: {}\n{}{}!"
"").format(slot, author.mention,
payout["phrase"], bid, then, now))
await channel.send(
_("{}\n{} {}\n\nYour bid: {}\n{}{}!").format(
slot, author.mention, payout["phrase"], bid, then, now
)
)
else:
then = await bank.get_balance(author)
await bank.withdraw_credits(author, bid)
now = then - bid
await channel.send(_("{}\n{} Nothing!\nYour bid: {}\n{}{}!"
"").format(slot, author.mention, bid, then, now))
await channel.send(
_("{}\n{} Nothing!\nYour bid: {}\n{}{}!").format(
slot, author.mention, bid, then, now
)
)
@commands.group()
@guild_only_check()
@@ -412,7 +446,6 @@ class Economy:
"""Changes economy module settings"""
guild = ctx.guild
if ctx.invoked_subcommand is None:
await ctx.send_help()
if await bank.is_global():
slot_min = await self.config.SLOT_MIN()
slot_max = await self.config.SLOT_MAX()
@@ -427,17 +460,18 @@ class Economy:
payday_amount = await self.config.guild(guild).PAYDAY_CREDITS()
register_amount = await bank.get_default_balance(guild)
msg = box(
_("Minimum slot bid: {}\n"
"Maximum slot bid: {}\n"
"Slot cooldown: {}\n"
"Payday amount: {}\n"
"Payday cooldown: {}\n"
"Amount given at account registration: {}"
"").format(
slot_min, slot_max, slot_time,
payday_amount, payday_time, register_amount
_(
"Minimum slot bid: {}\n"
"Maximum slot bid: {}\n"
"Slot cooldown: {}\n"
"Payday amount: {}\n"
"Payday cooldown: {}\n"
"Amount given at account registration: {}"
""
).format(
slot_min, slot_max, slot_time, payday_amount, payday_time, register_amount
),
_("Current Economy settings:")
_("Current Economy settings:"),
)
await ctx.send(msg)
@@ -445,7 +479,7 @@ class Economy:
async def slotmin(self, ctx: commands.Context, bid: int):
"""Minimum slot machine bid"""
if bid < 1:
await ctx.send(_('Invalid min bid amount.'))
await ctx.send(_("Invalid min bid amount."))
return
guild = ctx.guild
if await bank.is_global():
@@ -460,8 +494,7 @@ class Economy:
"""Maximum slot machine bid"""
slot_min = await self.config.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
guild = ctx.guild
credits_name = await bank.get_currency_name(guild)
@@ -489,8 +522,9 @@ class Economy:
await self.config.PAYDAY_TIME.set(seconds)
else:
await self.config.guild(guild).PAYDAY_TIME.set(seconds)
await ctx.send(_("Value modified. At least {} seconds must pass "
"between each payday.").format(seconds))
await ctx.send(
_("Value modified. At least {} seconds must pass between each payday.").format(seconds)
)
@economyset.command()
async def paydayamount(self, ctx: commands.Context, creds: int):
@@ -504,8 +538,7 @@ class Economy:
await self.config.PAYDAY_CREDITS.set(creds)
else:
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()
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
@@ -516,8 +549,11 @@ class Economy:
await ctx.send("The bank must be per-server for per-role paydays to work.")
else:
await self.config.role(role).PAYDAY_CREDITS.set(creds)
await ctx.send(_("Every payday will now give {} {} to people with the role {}."
"").format(creds, credits_name, role.name))
await ctx.send(
_("Every payday will now give {} {} to people with the role {}.").format(
creds, credits_name, role.name
)
)
@economyset.command()
async def registeramount(self, ctx: commands.Context, creds: int):
@@ -527,17 +563,18 @@ class Economy:
creds = 0
credits_name = await bank.get_currency_name(guild)
await bank.set_default_balance(creds, guild)
await ctx.send(_("Registering an account will now give {} {}."
"").format(creds, credits_name))
await ctx.send(
_("Registering an account will now give {} {}.").format(creds, credits_name)
)
# What would I ever do without stackoverflow?
def display_time(self, seconds, granularity=2):
intervals = ( # Source: http://stackoverflow.com/a/24542445
(_('weeks'), 604800), # 60 * 60 * 24 * 7
(_('days'), 86400), # 60 * 60 * 24
(_('hours'), 3600), # 60 * 60
(_('minutes'), 60),
(_('seconds'), 1),
(_("weeks"), 604800), # 60 * 60 * 24 * 7
(_("days"), 86400), # 60 * 60 * 24
(_("hours"), 3600), # 60 * 60
(_("minutes"), 60),
(_("seconds"), 1),
)
result = []
@@ -547,6 +584,6 @@ class Economy:
if value:
seconds -= value * count
if value == 1:
name = name.rstrip('s')
name = name.rstrip("s")
result.append("{} {}".format(value, name))
return ', '.join(result[:granularity])
return ", ".join(result[:granularity])

View File

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

View File

@@ -21,12 +21,9 @@ class Filter:
"filterban_count": 0,
"filterban_time": 0,
"filter_names": False,
"filter_default_name": "John Doe"
}
default_member_settings = {
"filter_count": 0,
"next_reset_time": 0
"filter_default_name": "John Doe",
}
default_member_settings = {"filter_count": 0, "next_reset_time": 0}
self.settings.register_guild(**default_guild_settings)
self.settings.register_member(**default_member_settings)
self.register_task = self.bot.loop.create_task(self.register_filterban())
@@ -37,8 +34,7 @@ class Filter:
async def register_filterban(self):
try:
await modlog.register_casetype(
"filterban", False, ":filing_cabinet: :hammer:",
"Filter ban", "ban"
"filterban", False, ":filing_cabinet: :hammer:", "Filter ban", "ban"
)
except RuntimeError:
pass
@@ -53,7 +49,6 @@ class Filter:
Using this command with no subcommands will send
the list of the server's filtered words."""
if ctx.invoked_subcommand is None:
await ctx.send_help()
server = ctx.guild
author = ctx.author
word_list = await self.settings.guild(server).filter()
@@ -79,13 +74,12 @@ class Filter:
word_list = []
tmp = ""
for word in split_words:
if not word.startswith("\"")\
and not word.endswith("\"") and not tmp:
if not word.startswith('"') and not word.endswith('"') and not tmp:
word_list.append(word)
else:
if word.startswith("\""):
if word.startswith('"'):
tmp += word[1:]
elif word.endswith("\""):
elif word.endswith('"'):
tmp += word[:-1]
word_list.append(tmp)
tmp = ""
@@ -110,13 +104,12 @@ class Filter:
word_list = []
tmp = ""
for word in split_words:
if not word.startswith("\"")\
and not word.endswith("\"") and not tmp:
if not word.startswith('"') and not word.endswith('"') and not tmp:
word_list.append(word)
else:
if word.startswith("\""):
if word.startswith('"'):
tmp += word[1:]
elif word.endswith("\""):
elif word.endswith('"'):
tmp += word[:-1]
word_list.append(tmp)
tmp = ""
@@ -130,47 +123,42 @@ class Filter:
@_filter.command(name="names")
async def filter_names(self, ctx: commands.Context):
"""
Toggles whether or not to check names and nicknames against the filter
"""Toggles whether or not to check names and nicknames against the filter
This is disabled by default
"""
guild = ctx.guild
current_setting = await self.settings.guild(guild).filter_names()
await self.settings.guild(guild).filter_names.set(not current_setting)
if current_setting:
await ctx.send(
_("Names and nicknames will no longer be "
"checked against the filter")
)
await ctx.send(_("Names and nicknames will no longer be checked against the filter."))
else:
await ctx.send(
_("Names and nicknames will now be checked against "
"the filter")
)
await ctx.send(_("Names and nicknames will now be checked against the filter."))
@_filter.command(name="defaultname")
async def filter_default_name(self, ctx: commands.Context, name: str):
"""
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
The default name used is John Doe
"""
guild = ctx.guild
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")
async def filter_ban(
self, ctx: commands.Context, count: int, timeframe: int):
"""
Sets up an autoban if the specified number of messages are
filtered in the specified amount of time (in seconds)
async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int):
"""Autobans if the specified number of messages are filtered in the timeframe
The timeframe is represented by seconds.
"""
if (count <= 0) != (timeframe <= 0):
await ctx.send(
_("Count and timeframe either both need to be 0 "
"or both need to be greater than 0!"
)
_(
"Count and timeframe either both need to be 0 "
"or both need to be greater than 0!"
)
)
return
elif count == 0 and timeframe == 0:
@@ -213,9 +201,7 @@ class Filter:
if filter_count > 0 and filter_time > 0:
if message.created_at.timestamp() >= next_reset_time:
next_reset_time = message.created_at.timestamp() + filter_time
await self.settings.member(author).next_reset_time.set(
next_reset_time
)
await self.settings.member(author).next_reset_time.set(next_reset_time)
if user_count > 0:
user_count = 0
await self.settings.member(author).filter_count.set(user_count)
@@ -231,17 +217,24 @@ class Filter:
if filter_count > 0 and filter_time > 0:
user_count += 1
await self.settings.member(author).filter_count.set(user_count)
if user_count >= filter_count and \
message.created_at.timestamp() < next_reset_time:
reason = "Autoban (too many filtered messages)"
if (
user_count >= filter_count
and message.created_at.timestamp() < next_reset_time
):
reason = "Autoban (too many filtered messages.)"
try:
await server.ban(author, reason=reason)
except:
pass
else:
await modlog.create_case(
self.bot, server, message.created_at,
"filterban", author, server.me, reason
self.bot,
server,
message.created_at,
"filterban",
author,
server.me,
reason,
)
async def on_message(self, message: discord.Message):
@@ -251,7 +244,7 @@ class Filter:
valid_user = isinstance(author, discord.Member) and not author.bot
if not valid_user:
return
# Bots and mods or superior are ignored from the filter
mod_or_superior = await is_mod_or_superior(self.bot, obj=author)
if mod_or_superior:
@@ -266,7 +259,7 @@ class Filter:
valid_user = isinstance(author, discord.Member) and not author.bot
if not valid_user:
return
# Bots and mods or superior are ignored from the filter
mod_or_superior = await is_mod_or_superior(self.bot, obj=author)
if mod_or_superior:
@@ -323,4 +316,3 @@ class Filter:
except:
pass
break

View File

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

View File

@@ -15,8 +15,8 @@ _ = Translator("General", __file__)
class RPS(Enum):
rock = "\N{MOYAI}"
paper = "\N{PAGE FACING UP}"
rock = "\N{MOYAI}"
paper = "\N{PAGE FACING UP}"
scissors = "\N{BLACK SCISSORS}"
@@ -40,13 +40,26 @@ class General:
def __init__(self):
self.stopwatches = {}
self.ball = [
_("As I see it, yes"), _("It is certain"), _("It is decidedly so"),
_("Most likely"), _("Outlook good"), _("Signs point to yes"),
_("Without a doubt"), _("Yes"), _("Yes definitely"), _("You may rely on it"),
_("Reply hazy, try again"), _("Ask again later"),
_("Better not tell you now"), _("Cannot predict now"),
_("Concentrate and ask again"), _("Don't count on it"), _("My reply is no"),
_("My sources say no"), _("Outlook not so good"), _("Very doubtful")
_("As I see it, yes"),
_("It is certain"),
_("It is decidedly so"),
_("Most likely"),
_("Outlook good"),
_("Signs point to yes"),
_("Without a doubt"),
_("Yes"),
_("Yes definitely"),
_("You may rely on it"),
_("Reply hazy, try again"),
_("Ask again later"),
_("Better not tell you now"),
_("Cannot predict now"),
_("Concentrate and ask again"),
_("Don't count on it"),
_("My reply is no"),
_("My sources say no"),
_("Outlook not so good"),
_("Very doubtful"),
]
@commands.command()
@@ -57,12 +70,12 @@ class General:
"""
choices = [escape(c, mass_mentions=True) for c in choices]
if len(choices) < 2:
await ctx.send(_('Not enough choices to pick from.'))
await ctx.send(_("Not enough choices to pick from."))
else:
await ctx.send(choice(choices))
@commands.command()
async def roll(self, ctx, number : int = 100):
async def roll(self, ctx, number: int = 100):
"""Rolls random number (between 1 and user choice)
Defaults to 100.
@@ -70,14 +83,12 @@ class General:
author = ctx.author
if number > 1:
n = randint(1, number)
await ctx.send(
_("{} :game_die: {} :game_die:").format(author.mention, n)
)
await ctx.send(_("{} :game_die: {} :game_die:").format(author.mention, n))
else:
await ctx.send(_("{} Maybe higher than 1? ;P").format(author.mention))
@commands.command()
async def flip(self, ctx, user: discord.Member=None):
async def flip(self, ctx, user: discord.Member = None):
"""Flips a coin... or a user.
Defaults to coin.
@@ -86,8 +97,7 @@ class General:
msg = ""
if user.id == ctx.bot.user.id:
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"
tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz"
table = str.maketrans(char, tran)
@@ -98,45 +108,37 @@ class General:
name = name.translate(table)
await ctx.send(msg + "(╯°□°)╯︵ " + name[::-1])
else:
await ctx.send(
_("*flips a coin and... ") + choice([_("HEADS!*"), _("TAILS!*")])
)
await ctx.send(_("*flips a coin and... ") + choice([_("HEADS!*"), _("TAILS!*")]))
@commands.command()
async def rps(self, ctx, your_choice : RPSParser):
async def rps(self, ctx, your_choice: RPSParser):
"""Play rock paper scissors"""
author = ctx.author
player_choice = your_choice.choice
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
cond = {
(RPS.rock, RPS.paper) : False,
(RPS.rock, RPS.scissors) : True,
(RPS.paper, RPS.rock) : True,
(RPS.paper, RPS.scissors) : False,
(RPS.scissors, RPS.rock) : False,
(RPS.scissors, RPS.paper) : True
}
(RPS.rock, RPS.paper): False,
(RPS.rock, RPS.scissors): True,
(RPS.paper, RPS.rock): True,
(RPS.paper, RPS.scissors): False,
(RPS.scissors, RPS.rock): False,
(RPS.scissors, RPS.paper): True,
}
if red_choice == player_choice:
outcome = None # Tie
outcome = None # Tie
else:
outcome = cond[(player_choice, red_choice)]
if outcome is True:
await ctx.send(_("{} You win {}!").format(
red_choice.value, author.mention
))
await ctx.send(_("{} You win {}!").format(red_choice.value, author.mention))
elif outcome is False:
await ctx.send(_("{} You lose {}!").format(
red_choice.value, author.mention
))
await ctx.send(_("{} You lose {}!").format(red_choice.value, author.mention))
else:
await ctx.send(_("{} We're square {}!").format(
red_choice.value, author.mention
))
await ctx.send(_("{} We're square {}!").format(red_choice.value, author.mention))
@commands.command(name="8", aliases=["8ball"])
async def _8ball(self, ctx, *, question : str):
async def _8ball(self, ctx, *, question: str):
"""Ask 8 ball a question
Question must end with a question mark.
@@ -160,14 +162,14 @@ class General:
self.stopwatches.pop(author.id, None)
@commands.command()
async def lmgtfy(self, ctx, *, search_terms : str):
async def lmgtfy(self, ctx, *, search_terms: str):
"""Creates a lmgtfy link"""
search_terms = escape(search_terms.replace(" ", "+"), mass_mentions=True)
await ctx.send("https://lmgtfy.com/?q={}".format(search_terms))
@commands.command(hidden=True)
@commands.guild_only()
async def hug(self, ctx, user : discord.Member, intensity : int=1):
async def hug(self, ctx, user: discord.Member, intensity: int = 1):
"""Because everyone likes hugs
Up to 10 intensity levels."""
@@ -184,97 +186,24 @@ class General:
msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name)
await ctx.send(msg)
@commands.command()
@commands.guild_only()
async def userinfo(self, ctx, *, user: discord.Member=None):
"""Shows users's informations"""
author = ctx.author
guild = ctx.guild
if not user:
user = author
# A special case for a special someone :^)
special_date = datetime.datetime(2016, 1, 10, 6, 8, 4, 443000)
is_special = (user.id == 96130341705637888 and
guild.id == 133049272517001216)
roles = sorted(user.roles)[1:]
joined_at = user.joined_at if not is_special else special_date
since_created = (ctx.message.created_at - user.created_at).days
since_joined = (ctx.message.created_at - joined_at).days
user_joined = joined_at.strftime("%d %b %Y %H:%M")
user_created = user.created_at.strftime("%d %b %Y %H:%M")
member_number = sorted(guild.members,
key=lambda m: m.joined_at).index(user) + 1
created_on = _("{}\n({} days ago)").format(user_created, since_created)
joined_on = _("{}\n({} days ago)").format(user_joined, since_joined)
activity = _("Chilling in {} status").format(user.status)
if user.activity is None: # Default status
pass
elif user.activity.type == discord.ActivityType.playing:
activity = _("Playing {}").format(user.activity.name)
elif user.activity.type == discord.ActivityType.streaming:
activity = _("Streaming [{}]({})").format(user.activity.name, user.activity.url)
elif user.activity.type == discord.ActivityType.listening:
activity = _("Listening to {}").format(user.activity.name)
elif user.activity.type == discord.ActivityType.watching:
activity = _("Watching {}").format(user.activity.name)
if roles:
roles = ", ".join([x.name for x in roles])
else:
roles = _("None")
data = discord.Embed(description=activity, colour=user.colour)
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=_("Roles"), value=roles, inline=False)
data.set_footer(text=_("Member #{} | User ID: {}"
"").format(member_number, user.id))
name = str(user)
name = " ~ ".join((name, user.nick)) if user.nick else name
if user.avatar:
avatar = user.avatar_url
avatar = avatar.replace('webp', 'png')
data.set_author(name=name, url=avatar)
data.set_thumbnail(url=avatar)
else:
data.set_author(name=name)
try:
await ctx.send(embed=data)
except discord.HTTPException:
await ctx.send(_("I need the `Embed links` permission "
"to send this."))
@commands.command()
@commands.guild_only()
async def serverinfo(self, ctx):
"""Shows server's informations"""
guild = ctx.guild
online = len([m.status for m in guild.members
if m.status == discord.Status.online or
m.status == discord.Status.idle])
online = len([m.status for m in guild.members if m.status != discord.Status.offline])
total_users = len(guild.members)
text_channels = len(guild.text_channels)
voice_channels = len(guild.voice_channels)
passed = (ctx.message.created_at - guild.created_at).days
created_at = (_("Since {}. That's over {} days ago!"
"").format(guild.created_at.strftime("%d %b %Y %H:%M"),
passed))
created_at = _("Since {}. That's over {} days ago!").format(
guild.created_at.strftime("%d %b %Y %H:%M"), passed
)
colour = ''.join([choice('0123456789ABCDEF') for x in range(6)])
colour = "".join([choice("0123456789ABCDEF") for x in range(6)])
colour = randint(0, 0xFFFFFF)
data = discord.Embed(
description=created_at,
colour=discord.Colour(value=colour))
data = discord.Embed(description=created_at, colour=discord.Colour(value=colour))
data.add_field(name=_("Region"), value=str(guild.region))
data.add_field(name=_("Users"), value="{}/{}".format(online, total_users))
data.add_field(name=_("Text Channels"), value=text_channels)
@@ -292,16 +221,16 @@ class General:
try:
await ctx.send(embed=data)
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()
async def urban(self, ctx, *, search_terms: str, definition_number: int=1):
async def urban(self, ctx, *, search_terms: str, definition_number: int = 1):
"""Urban Dictionary search
Definition number must be between 1 and 10"""
def encode(s):
return quote_plus(s, encoding='utf-8', errors='replace')
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
@@ -313,8 +242,8 @@ class General:
search_terms = search_terms[:-1]
else:
pos = 0
if pos not in range(0, 11): # API only provides the
pos = 0 # top 10 definitions
if pos not in range(0, 11): # API only provides the
pos = 0 # top 10 definitions
except ValueError:
pos = 0
@@ -326,18 +255,18 @@ class General:
result = await r.json()
item_list = result["list"]
if item_list:
definition = item_list[pos]['definition']
example = item_list[pos]['example']
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 = "**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))
await ctx.send(_("There is no definition #{}").format(pos + 1))
except:
await ctx.send(_("Error."))

View File

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

View File

@@ -13,9 +13,8 @@ GIPHY_API_KEY = "dc6zaTOxFJmzC"
@cog_i18n(_)
class Image:
"""Image related commands."""
default_global = {
"imgur_client_id": None
}
default_global = {"imgur_client_id": None}
def __init__(self, bot):
self.bot = bot
@@ -33,8 +32,7 @@ class Image:
Make sure to set the client ID using
[p]imgurcreds"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
pass
@_imgur.command(name="search")
async def imgur_search(self, ctx, *, term: str):
@@ -44,8 +42,10 @@ class Image:
imgur_client_id = await self.settings.imgur_client_id()
if not imgur_client_id:
await ctx.send(
_("A client ID has not been set! Please set one with {}").format(
"`{}imgurcreds`".format(ctx.prefix)))
_("A client ID has not been set! Please set one with {}.").format(
"`{}imgurcreds`".format(ctx.prefix)
)
)
return
headers = {"Authorization": "Client-ID {}".format(imgur_client_id)}
async with self.session.get(url, headers=headers, params=params) as search_get:
@@ -54,7 +54,7 @@ class Image:
if data["success"]:
results = data["data"]
if not results:
await ctx.send(_("Your search returned no results"))
await ctx.send(_("Your search returned no results."))
return
shuffle(results)
msg = _("Search results...\n")
@@ -63,10 +63,12 @@ class Image:
msg += "\n"
await ctx.send(msg)
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")
async def imgur_subreddit(self, ctx, subreddit: str, sort_type: str="top", window: str="day"):
async def imgur_subreddit(
self, ctx, subreddit: str, sort_type: str = "top", window: str = "day"
):
"""Gets images from the specified subreddit section
Sort types: new, top
@@ -89,8 +91,10 @@ class Image:
imgur_client_id = await self.settings.imgur_client_id()
if not imgur_client_id:
await ctx.send(
_("A client ID has not been set! Please set one with {}").format(
"`{}imgurcreds`".format(ctx.prefix)))
_("A client ID has not been set! Please set one with {}.").format(
"`{}imgurcreds`".format(ctx.prefix)
)
)
return
links = []
@@ -112,12 +116,13 @@ class Image:
else:
await ctx.send(_("No results found."))
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()
@commands.command()
async def imgurcreds(self, ctx, imgur_client_id: str):
"""Sets the imgur client id
You will need an account on Imgur to get this
You can get these by visiting https://api.imgur.com/oauth2/addclient
@@ -126,7 +131,7 @@ class Image:
set the authorization callback url to 'https://localhost'
leave the app website blank, enter a valid email address, and
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 ctx.send(_("Set the imgur client id!"))
@@ -139,8 +144,9 @@ class Image:
await ctx.send_help()
return
url = ("http://api.giphy.com/v1/gifs/search?&api_key={}&q={}"
"".format(GIPHY_API_KEY, keywords))
url = "http://api.giphy.com/v1/gifs/search?&api_key={}&q={}".format(
GIPHY_API_KEY, keywords
)
async with self.session.get(url) as r:
result = await r.json()
@@ -150,7 +156,7 @@ class Image:
else:
await ctx.send(_("No results found."))
else:
await ctx.send(_("Error contacting the API"))
await ctx.send(_("Error contacting the API."))
@commands.command(pass_context=True, no_pm=True)
async def gifr(self, ctx, *keywords):
@@ -161,8 +167,9 @@ class Image:
await ctx.send_help()
return
url = ("http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}"
"".format(GIPHY_API_KEY, keywords))
url = "http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}".format(
GIPHY_API_KEY, keywords
)
async with self.session.get(url) as r:
result = await r.json()
@@ -172,4 +179,4 @@ class Image:
else:
await ctx.send(_("No results found."))
else:
await ctx.send(_("Error contacting the API"))
await ctx.send(_("Error contacting the API."))

View File

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

View File

@@ -1,4 +1,4 @@
from discord.ext import commands
from redbot.core import commands
import discord
@@ -18,11 +18,14 @@ def mod_or_voice_permissions(**perms):
for vc in guild.voice_channels:
resolved = vc.permissions_for(author)
good = all(getattr(resolved, name, None) == value for name, value in perms.items())
good = resolved.administrator or all(
getattr(resolved, name, None) == value for name, value in perms.items()
)
if not good:
return False
else:
return True
return commands.check(pred)
@@ -37,11 +40,14 @@ def admin_or_voice_permissions(**perms):
return True
for vc in guild.voice_channels:
resolved = vc.permissions_for(author)
good = all(getattr(resolved, name, None) == value for name, value in perms.items())
good = resolved.administrator or all(
getattr(resolved, name, None) == value for name, value in perms.items()
)
if not good:
return False
else:
return True
return commands.check(pred)
@@ -50,9 +56,12 @@ def bot_has_voice_permissions(**perms):
guild = ctx.guild
for vc in guild.voice_channels:
resolved = vc.permissions_for(guild.me)
good = all(getattr(resolved, name, None) == value for name, value in perms.items())
good = resolved.administrator or all(
getattr(resolved, name, None) == value for name, value in perms.items()
)
if not good:
return False
else:
return True
return commands.check(pred)

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -5,7 +5,7 @@ from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box
_ = Translator('ModLog', __file__)
_ = Translator("ModLog", __file__)
@cog_i18n(_)
@@ -19,8 +19,7 @@ class ModLog:
@checks.guildowner_or_permissions(administrator=True)
async def modlogset(self, ctx: commands.Context):
"""Settings for the mod log"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
pass
@modlogset.command()
@commands.guild_only()
@@ -32,15 +31,10 @@ class ModLog:
if channel:
if channel.permissions_for(guild.me).send_messages:
await modlog.set_modlog_channel(guild, channel)
await ctx.send(
_("Mod events will be sent to {}").format(
channel.mention
)
)
await ctx.send(_("Mod events will be sent to {}").format(channel.mention))
else:
await ctx.send(
_("I do not have permissions to "
"send messages in {}!").format(channel.mention)
_("I do not have permissions to send messages in {}!").format(channel.mention)
)
else:
try:
@@ -51,7 +45,7 @@ class ModLog:
await modlog.set_modlog_channel(guild, None)
await ctx.send(_("Mod log deactivated."))
@modlogset.command(name='cases')
@modlogset.command(name="cases")
@commands.guild_only()
async def set_cases(self, ctx: commands.Context, action: str = None):
"""Enables or disables case creation for each type of mod action"""
@@ -64,8 +58,8 @@ class ModLog:
msg = ""
for ct in casetypes:
enabled = await ct.is_enabled()
value = 'enabled' if enabled else 'disabled'
msg += '%s : %s\n' % (ct.name, value)
value = "enabled" if enabled else "disabled"
msg += "%s : %s\n" % (ct.name, value)
msg = title + "\n" + box(msg)
await ctx.send(msg)
@@ -78,10 +72,8 @@ class ModLog:
enabled = await casetype.is_enabled()
await casetype.set_enabled(True if not enabled else False)
msg = (
_('Case creation for {} actions is now {}.').format(
action, 'enabled' if not enabled else 'disabled'
)
msg = _("Case creation for {} actions is now {}.").format(
action, "enabled" if not enabled else "disabled"
)
await ctx.send(msg)
@@ -133,8 +125,10 @@ class ModLog:
if audit_type:
audit_case = None
async for entry in guild.audit_logs(action=audit_type):
if entry.target.id == case_before.user.id and \
entry.action == audit_type:
if (
entry.target.id == case_before.user.id
and entry.action == audit_type
):
audit_case = entry
break
if audit_case:
@@ -145,11 +139,9 @@ class ModLog:
if not (is_guild_owner or is_case_author or author_is_mod):
await ctx.send(_("You are not authorized to modify that case!"))
return
to_modify = {
"reason": reason,
}
to_modify = {"reason": reason}
if case_before.moderator != author:
to_modify["amended_by"] = author
to_modify["modified_at"] = ctx.message.created_at.timestamp()
await case_before.edit(self.bot, to_modify)
await case_before.edit(to_modify)
await ctx.send(_("Reason has been updated."))

View File

@@ -0,0 +1,5 @@
from .permissions import Permissions
def setup(bot):
bot.add_cog(Permissions(bot))

View File

@@ -0,0 +1,44 @@
from redbot.core import commands
from typing import Tuple
class CogOrCommand(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> Tuple[str]:
ret = ctx.bot.get_cog(arg)
if ret:
return "cogs", ret.__class__.__name__
ret = ctx.bot.get_command(arg)
if ret:
return "commands", ret.qualified_name
raise commands.BadArgument(
'Cog or command "{arg}" not found. Please note that this is case sensitive.'
"".format(arg=arg)
)
class RuleType(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"
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

@@ -0,0 +1,669 @@
from copy import copy
import contextlib
import asyncio
import discord
from redbot.core import commands
from redbot.core.bot import Red
from redbot.core import checks
from redbot.core.config import Config
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, entries_from_ctx
from .yaml_handler import yamlset_acl, yamlget_acl
from .converters import CogOrCommand, RuleType, ClearableRuleType
from .mass_resolution import mass_resolve
_models = ["owner", "guildowner", "admin", "mod", "all"]
_ = Translator("Permissions", __file__)
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(_)
class Permissions:
"""
A high level permission model
"""
# Not sure if we will use admin or mod models in core red
# but they are explicitly supported
resolution_order = {k: _models[:i] for i, k in enumerate(_models, 1)}
def __init__(self, bot: Red):
self.bot = bot
self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True)
self.config.register_global(owner_models={})
self.config.register_guild(owner_models={})
self.cache = LRUDict(size=25000) # This can be tuned later
async def get_user_ctx_overrides(self, ctx: commands.Context) -> dict:
"""
This takes a context object, and returns a dict of
allowed: list of commands
denied: list of commands
default: list of commands
representing how permissions interacts with the
user, channel, guild, and (possibly) voice channel
for all commands on the bot (not just the one in the context object)
This mainly exists for use by the help formatter,
but others may find it useful
Unlike the rest of the permission system, if other models are added later,
due to optimizations made for this, this needs to be adjusted accordingly
This does not account for before and after permission hooks,
these need to be checked seperately
"""
return await mass_resolve(ctx=ctx, config=self.config)
async def __global_check(self, ctx: commands.Context) -> bool:
"""
Yes, this is needed on top of hooking into checks.py
to ensure that unchecked commands can still be managed by permissions
This should return True in the case of no overrides
defering to check logic
This works since all checks must be True to run
"""
v = await self.check_overrides(ctx, "all")
if v is False:
return False
return True
async def check_overrides(self, ctx: commands.Context, level: str) -> bool:
"""
This checks for any overrides in the permission model
Parameters
----------
ctx: `redbot.core.context.commands.Context`
The context of the command
level: `str`
One of 'owner', 'guildowner', 'admin', 'mod', 'all'
Returns
-------
bool
a trinary value using None + bool to resolve permissions for
checks.py
"""
if await ctx.bot.is_owner(ctx.author):
return True
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)
if override is not None:
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]:
if ctx.guild is None and model != "owner":
break
override_model = getattr(self, model + "_model", None)
override = await override_model(ctx) if override_model else None
if override is not None:
self.cache[cache_tup] = override
return override
# This is intentional not being in an else block
self.cache[cache_tup] = None
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)
if override is not None:
return override
return None
async def owner_model(self, ctx: commands.Context) -> bool:
"""
Handles owner level overrides
"""
async with self.config.owner_models() as models:
return resolve_models(ctx=ctx, models=models)
async def guildowner_model(self, ctx: commands.Context) -> bool:
"""
Handles guild level overrides
"""
if ctx.guild is None:
return None
async with self.config.guild(ctx.guild).owner_models() as models:
return resolve_models(ctx=ctx, models=models)
# Either of the below function signatures could be used
# without any other modifications required at a later date
#
# async def admin_model(self, ctx: commands.Context) -> bool:
# async def mod_model(self, ctx: commands.Context) -> bool:
@commands.group(aliases=["p"])
async def permissions(self, ctx: commands.Context):
"""
Permission management tools
"""
pass
@permissions.command()
async def explain(self, ctx: commands.Context):
"""
Provides a detailed explanation of how the permission model functions
"""
# Apologies in advance for the translators out there...
message = _(
"This cog extends the default permission model of the bot. "
"By default, many commands are restricted based on what "
"the command can do."
"\n"
"Any command that could impact the host machine, "
"is generally owner only."
"\n"
"Commands that take administrative or moderator "
"actions in servers generally require a mod or an admin."
"\n"
"This cog allows you to refine some of those settings. "
"You can allow wider or narrower "
"access to most commands using it."
"\n\n"
"When additional rules are set using this cog, "
"those rules will be checked prior to "
"checking for the default restrictions of the command. "
"\n"
"Rules set globally (by the owner) are checked first, "
"then rules set for guilds. If multiple global or guild "
"rules apply to the case, the order they are checked is:"
"\n"
"1. Rules about a user.\n"
"2. Rules about the voice channel a user is in.\n"
"3. Rules about the text channel a command was issued in.\n"
"4. Rules about a role the user has "
"(The highest role they have with a rule will be used).\n"
"5. Rules about the guild a user is in (Owner level only)."
"\n\nFor more details, please read the official documentation."
)
await ctx.maybe_send_embed(message)
@permissions.command(name="canrun")
async def _test_permission_model(
self, ctx: commands.Context, user: discord.Member, *, command: str
):
"""
This checks if someone can run a command in the current location
"""
if not command:
return await ctx.send_help()
message = copy(ctx.message)
message.author = user
message.content = "{}{}".format(ctx.prefix, command)
com = self.bot.get_command(command)
if com is None:
out = _("No such command")
else:
try:
testcontext = await self.bot.get_context(message, cls=commands.Context)
can = await com.can_run(testcontext) and all(
[await p.can_run(testcontext) for p in com.parents]
)
except commands.CheckFailure:
can = False
out = (
_("That user can run the specified command.")
if can
else _("That user can not run the specified command.")
)
await ctx.send(out)
@checks.is_owner()
@permissions.command(name="setglobalacl")
async def owner_set_acl(self, ctx: commands.Context):
"""
Take a YAML file upload to set permissions from
"""
if not ctx.message.attachments:
return await ctx.send(_("You must upload a file."))
try:
await yamlset_acl(ctx, config=self.config.owner_models, update=False)
except Exception as e:
print(e)
return await ctx.send(_("Invalid syntax."))
else:
await ctx.send(_("Rules set."))
self.invalidate_cache()
@checks.is_owner()
@permissions.command(name="getglobalacl")
async def owner_get_acl(self, ctx: commands.Context):
"""
Dumps a YAML file with the current owner level permissions
"""
await yamlget_acl(ctx, config=self.config.owner_models)
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
@permissions.command(name="setguildacl")
async def guild_set_acl(self, ctx: commands.Context):
"""
Take a YAML file upload to set permissions from
"""
if not ctx.message.attachments:
return await ctx.send(_("You must upload a file."))
try:
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=False)
except Exception as e:
print(e)
return await ctx.send(_("Invalid syntax."))
else:
await ctx.send(_("Rules set."))
self.invalidate_cache(ctx.guild.id)
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
@permissions.command(name="getguildacl")
async def guild_get_acl(self, ctx: commands.Context):
"""
Dumps a YAML file with the current owner level permissions
"""
await yamlget_acl(ctx, config=self.config.guild(ctx.guild).owner_models)
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
@permissions.command(name="updateguildacl")
async def guild_update_acl(self, ctx: commands.Context):
"""
Take a YAML file upload to update permissions from
Use this to not lose existing rules
"""
if not ctx.message.attachments:
return await ctx.send(_("You must upload a file."))
try:
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=True)
except Exception as e:
print(e)
return await ctx.send(_("Invalid syntax."))
else:
await ctx.send(_("Rules set."))
self.invalidate_cache(ctx.guild.id)
@checks.is_owner()
@permissions.command(name="updateglobalacl")
async def owner_update_acl(self, ctx: commands.Context):
"""
Take a YAML file upload to update permissions from
Use this to not lose existing rules
"""
if not ctx.message.attachments:
return await ctx.send(_("You must upload a file."))
try:
await yamlset_acl(ctx, config=self.config.owner_models, update=True)
except Exception as e:
print(e)
return await ctx.send(_("Invalid syntax."))
else:
await ctx.send(_("Rules set."))
self.invalidate_cache()
@checks.is_owner()
@permissions.command(name="addglobalrule")
async def add_to_global_rule(
self,
ctx: commands.Context,
allow_or_deny: RuleType,
cog_or_command: CogOrCommand,
who_or_what: str,
):
"""
Adds something to the rules
allow_or_deny: "allow" or "deny", depending on the rule to modify
cog_or_command: case sensitive cog or command name
nested commands should be space seperated, but enclosed in quotes
who_or_what: what to add to the rule list.
For best results, use an ID or mention
The bot will try to uniquely match even without,
but a failure to do so will raise an error
This can be a user, role, channel, or guild
"""
obj = self.find_object_uniquely(who_or_what)
if not obj:
return await ctx.send(_("No unique matches. Try using an ID or mention."))
model_type, type_name = cog_or_command
async with self.config.owner_models() as models:
data = {k: v for k, v in models.items()}
if model_type not in data:
data[model_type] = {}
if type_name not in data[model_type]:
data[model_type][type_name] = {}
if allow_or_deny not in data[model_type][type_name]:
data[model_type][type_name][allow_or_deny] = []
if obj in data[model_type][type_name][allow_or_deny]:
return await ctx.send(_("That rule already exists."))
data[model_type][type_name][allow_or_deny].append(obj)
models.update(data)
await ctx.send(_("Rule added."))
self.invalidate_cache(type_name, obj)
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
@permissions.command(name="addguildrule")
async def add_to_guild_rule(
self,
ctx: commands.Context,
allow_or_deny: RuleType,
cog_or_command: CogOrCommand,
who_or_what: str,
):
"""
Adds something to the rules
allow_or_deny: "allow" or "deny", depending on the rule to modify
cog_or_command: case sensitive cog or command name
nested commands should be space seperated, but enclosed in quotes
who_or_what: what to add to the rule list.
For best results, use an ID or mention
The bot will try to uniquely match even without,
but a failure to do so will raise an error
This can be a user, role, channel, or guild
"""
obj = self.find_object_uniquely(who_or_what)
if not obj:
return await ctx.send(_("No unique matches. Try using an ID or mention."))
model_type, type_name = cog_or_command
async with self.config.guild(ctx.guild).owner_models() as models:
data = {k: v for k, v in models.items()}
if model_type not in data:
data[model_type] = {}
if type_name not in data[model_type]:
data[model_type][type_name] = {}
if allow_or_deny not in data[model_type][type_name]:
data[model_type][type_name][allow_or_deny] = []
if obj in data[model_type][type_name][allow_or_deny]:
return await ctx.send(_("That rule already exists."))
data[model_type][type_name][allow_or_deny].append(obj)
models.update(data)
await ctx.send(_("Rule added."))
self.invalidate_cache(type_name, obj)
@checks.is_owner()
@permissions.command(name="removeglobalrule")
async def rem_from_global_rule(
self,
ctx: commands.Context,
allow_or_deny: RuleType,
cog_or_command: CogOrCommand,
who_or_what: str,
):
"""
removes something from the rules
allow_or_deny: "allow" or "deny", depending on the rule to modify
cog_or_command: case sensitive cog or command name
nested commands should be space seperated, but enclosed in quotes
who_or_what: what to add to the rule list.
For best results, use an ID or mention
The bot will try to uniquely match even without,
but a failure to do so will raise an error
This can be a user, role, channel, or guild
"""
obj = self.find_object_uniquely(who_or_what)
if not obj:
return await ctx.send(_("No unique matches. Try using an ID or mention."))
model_type, type_name = cog_or_command
async with self.config.owner_models() as models:
data = {k: v for k, v in models.items()}
if model_type not in data:
data[model_type] = {}
if type_name not in data[model_type]:
data[model_type][type_name] = {}
if allow_or_deny not in data[model_type][type_name]:
data[model_type][type_name][allow_or_deny] = []
if obj not in data[model_type][type_name][allow_or_deny]:
return await ctx.send(_("That rule doesn't exist."))
data[model_type][type_name][allow_or_deny].remove(obj)
models.update(data)
await ctx.send(_("Rule removed."))
self.invalidate_cache(obj, type_name)
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
@permissions.command(name="removeguildrule")
async def rem_from_guild_rule(
self,
ctx: commands.Context,
allow_or_deny: RuleType,
cog_or_command: CogOrCommand,
who_or_what: str,
):
"""
removes something from the rules
allow_or_deny: "allow" or "deny", depending on the rule to modify
cog_or_command: case sensitive cog or command name
nested commands should be space seperated, but enclosed in quotes
who_or_what: what to add to the rule list.
For best results, use an ID or mention
The bot will try to uniquely match even without,
but a failure to do so will raise an error
This can be a user, role, channel, or guild
"""
obj = self.find_object_uniquely(who_or_what)
if not obj:
return await ctx.send(_("No unique matches. Try using an ID or mention."))
model_type, type_name = cog_or_command
async with self.config.guild(ctx.guild).owner_models() as models:
data = {k: v for k, v in models.items()}
if model_type not in data:
data[model_type] = {}
if type_name not in data[model_type]:
data[model_type][type_name] = {}
if allow_or_deny not in data[model_type][type_name]:
data[model_type][type_name][allow_or_deny] = []
if obj not in data[model_type][type_name][allow_or_deny]:
return await ctx.send(_("That rule doesn't exist."))
data[model_type][type_name][allow_or_deny].remove(obj)
models.update(data)
await ctx.send(_("Rule removed."))
self.invalidate_cache(obj, type_name)
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
@permissions.command(name="setdefaultguildrule")
async def set_default_guild_rule(
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
"""
val_to_set = {"allow": True, "deny": False, "clear": None}.get(allow_or_deny)
model_type, type_name = cog_or_command
async with self.config.guild(ctx.guild).owner_models() as models:
data = {k: v for k, v in models.items()}
if model_type not in data:
data[model_type] = {}
if type_name not in data[model_type]:
data[model_type][type_name] = {}
data[model_type][type_name]["default"] = val_to_set
models.update(data)
await ctx.send(_("Default set."))
self.invalidate_cache(type_name)
@checks.is_owner()
@permissions.command(name="setdefaultglobalrule")
async def set_default_global_rule(
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
"""
val_to_set = {"allow": True, "deny": False, "clear": None}.get(allow_or_deny)
model_type, type_name = cog_or_command
async with self.config.owner_models() as models:
data = {k: v for k, v in models.items()}
if model_type not in data:
data[model_type] = {}
if type_name not in data[model_type]:
data[model_type][type_name] = {}
data[model_type][type_name]["default"] = val_to_set
models.update(data)
await ctx.send(_("Default set."))
self.invalidate_cache(type_name)
@checks.is_owner()
@permissions.command(name="clearglobalsettings")
async def clear_globals(self, ctx: commands.Context):
"""
Clears all global rules.
"""
await self._confirm_then_clear_rules(ctx, is_guild=False)
self.invalidate_cache()
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
@permissions.command(name="clearguildsettings")
async def clear_guild_settings(self, ctx: commands.Context):
"""
Clears all guild rules.
"""
await self._confirm_then_clear_rules(ctx, is_guild=True)
self.invalidate_cache(ctx.guild.id)
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():
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."))
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 ctx.send(_("Guild settings cleared."))
else:
await self.config.owner_models.clear()
await ctx.send(_("Global settings cleared."))
else:
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:
"""
Finds an object uniquely, returns it's id or returns None
"""
if info is None:
return None
objs = []
objs.extend(self.bot.users)
for guild in self.bot.guilds:
objs.extend(guild.roles)
objs.extend(guild.channels)
try:
_id = int(info)
except ValueError:
_id = None
for function in (
lambda x: x.id == _id,
lambda x: x.mention == info,
lambda x: str(x) == info,
lambda x: x.name == info,
lambda x: (x.nick if hasattr(x, "nick") else None) == info,
):
canidates = list(filter(function, objs))
if len(canidates) == 1:
return canidates[0].id
return None

View File

@@ -0,0 +1,81 @@
import types
import contextlib
import asyncio
import logging
from redbot.core import commands
log = logging.getLogger("redbot.cogs.permissions.resolvers")
def entries_from_ctx(ctx: commands.Context) -> tuple:
voice_channel = None
with contextlib.suppress(Exception):
voice_channel = ctx.author.voice.voice_channel
entries = [x.id for x in (ctx.author, voice_channel, ctx.channel) if x]
roles = sorted(ctx.author.roles, reverse=True) if ctx.guild else []
entries.extend([x.id for x in roles])
# entries now contains the following (in order) (if applicable)
# author.id
# author.voice.voice_channel.id
# channel.id
# role.id for each role (highest to lowest)
# (implicitly) guild.id because
# the @everyone role shares an id with the guild
return tuple(entries)
async def val_if_check_is_valid(*, ctx: commands.Context, check: object, level: str) -> bool:
"""
Returns the value from a check if it is valid
"""
val = None
# let's not spam the console with improperly made 3rd party checks
try:
if asyncio.iscoroutinefunction(check):
val = await check(ctx, level=level)
else:
val = check(ctx, level=level)
except Exception as e:
# but still provide a way to view it (run with debug flag)
log.debug(str(e))
return val
def resolve_models(*, ctx: commands.Context, models: dict) -> bool:
"""
Resolves models in order.
"""
cmd_name = ctx.command.qualified_name
cog_name = ctx.cog.__class__.__name__
resolved = None
to_iter = (("commands", cmd_name), ("cogs", cog_name))
for model_name, ctx_attr in to_iter:
if ctx_attr in models.get(model_name, {}):
blacklist = models[model_name][ctx_attr].get("deny", [])
whitelist = models[model_name][ctx_attr].get("allow", [])
resolved = resolve_lists(ctx=ctx, whitelist=whitelist, blacklist=blacklist)
if resolved is not None:
return resolved
resolved = models[model_name][ctx_attr].get("default", None)
if resolved is not None:
return resolved
return None
def resolve_lists(*, ctx: commands.Context, whitelist: list, blacklist: list) -> bool:
"""
resolves specific lists
"""
for entry in entries_from_ctx(ctx):
if entry in whitelist:
return True
if entry in blacklist:
return False
return None

View File

@@ -0,0 +1,19 @@
cogs:
Admin:
allow:
- 78631113035100160
deny:
- 96733288462286848
Audio:
allow:
- 133049272517001216
default: deny
commands:
cleanup bot:
allow:
- 78631113035100160
default: deny
ping:
deny:
- 96733288462286848
default: allow

View File

@@ -0,0 +1,67 @@
import io
import yaml
import pathlib
import discord
def yaml_template() -> dict:
template_fp = pathlib.Path(__file__).parent / "template.yaml"
with template_fp.open() as f:
return yaml.safe_load(f)
async def yamlset_acl(ctx, *, config, update):
_fp = io.BytesIO()
await ctx.message.attachments[0].save(_fp)
try:
data = yaml.safe_load(_fp)
except yaml.YAMLError:
_fp.close()
del _fp
raise
old_data = await config()
for outer, inner in data.items():
for ok, iv in inner.items():
for k, v in iv.items():
if k == "default":
data[outer][ok][k] = {"allow": True, "deny": False}.get(v.lower(), None)
if not update:
continue
try:
if isinstance(old_data[outer][ok][k], list):
data[outer][ok][k].extend(old_data[outer][ok][k])
except KeyError:
pass
await config.set(data)
async def yamlget_acl(ctx, *, config):
data = await config()
removals = []
for outer, inner in data.items():
for ok, iv in inner.items():
for k, v in iv.items():
if k != "default":
continue
if v is True:
data[outer][ok][k] = "allow"
elif v is False:
data[outer][ok][k] = "deny"
else:
removals.append((outer, ok, k))
for tup in removals:
o, i, k = tup
data[o][i].pop(k, None)
_fp = io.BytesIO(yaml.dump(data, default_flow_style=False).encode())
_fp.seek(0)
await ctx.author.send(file=discord.File(_fp, filename="acl.yaml"))
_fp.close()

View File

@@ -22,15 +22,9 @@ log = logging.getLogger("red.reports")
@cog_i18n(_)
class Reports:
default_guild_settings = {
"output_channel": None,
"active": False,
"next_ticket": 1
}
default_guild_settings = {"output_channel": None, "active": False, "next_ticket": 1}
default_report = {
'report': {}
}
default_report = {"report": {}}
# This can be made configureable later if it
# becomes an issue.
@@ -42,15 +36,14 @@ class Reports:
(timedelta(seconds=5), 1),
(timedelta(minutes=5), 3),
(timedelta(hours=1), 10),
(timedelta(days=1), 24)
(timedelta(days=1), 24),
]
def __init__(self, bot: Red):
self.bot = bot
self.config = Config.get_conf(
self, 78631113035100160, force_registration=True)
self.config = Config.get_conf(self, 78631113035100160, force_registration=True)
self.config.register_guild(**self.default_guild_settings)
self.config.register_custom('REPORT', **self.default_report)
self.config.register_custom("REPORT", **self.default_report)
self.antispam = {}
self.user_cache = []
self.tunnel_store = {}
@@ -59,38 +52,36 @@ class Reports:
@property
def tunnels(self):
return [
x['tun'] for x in self.tunnel_store.values()
]
return [x["tun"] for x in self.tunnel_store.values()]
@checks.admin_or_permissions(manage_guild=True)
@commands.guild_only()
@commands.group(name="reportset")
async def reportset(self, ctx: commands.Context):
"""
settings for reports
Settings for the report system.
"""
pass
@checks.admin_or_permissions(manage_guild=True)
@reportset.command(name="output")
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 ctx.send(_("Report Channel Set."))
await ctx.send(_("The report channel has been set."))
@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):
"""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 = not active
await self.config.guild(ctx.guild).active.set(active)
if active:
await ctx.send(_("Reporting now enabled"))
await ctx.send(_("Reporting is now enabled"))
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):
ret = False
@@ -99,9 +90,7 @@ class Reports:
admin_role = discord.utils.get(
guild.roles, id=await self.bot.db.guild(guild).admin_role()
)
mod_role = discord.utils.get(
guild.roles, id=await self.bot.db.guild(guild).mod_role()
)
mod_role = discord.utils.get(guild.roles, id=await self.bot.db.guild(guild).mod_role())
ret |= any(r in m.roles for r in (mod_role, admin_role))
if perms:
ret |= m.guild_permissions >= perms
@@ -111,10 +100,13 @@ class Reports:
return ret
async def discover_guild(
self, author: discord.User, *,
mod: bool=False,
permissions: Union[discord.Permissions, dict]=None,
prompt: str=""):
self,
author: discord.User,
*,
mod: bool = False,
permissions: Union[discord.Permissions, dict] = None,
prompt: str = "",
):
"""
discovers which of shared guilds between the bot
and provided user based on conditions (mod or permissions is an or)
@@ -151,13 +143,9 @@ class Reports:
return m.author == author and m.channel == dm.channel
try:
message = await self.bot.wait_for(
'message', check=pred, timeout=45
)
message = await self.bot.wait_for("message", check=pred, timeout=45)
except asyncio.TimeoutError:
await author.send(
_("You took too long to select. Try again later.")
)
await author.send(_("You took too long to select. Try again later."))
return None
try:
@@ -187,82 +175,70 @@ class Reports:
if await self.bot.embed_requested(channel, author):
em = discord.Embed(description=report)
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))
send_content = None
else:
em = None
send_content = _(
'Report from {author.mention} (Ticket #{number})'
).format(author=author, number=ticket_number)
send_content = _("Report from {author.mention} (Ticket #{number})").format(
author=author, number=ticket_number
)
send_content += "\n" + report
try:
await Tunnel.message_forwarder(
destination=channel,
content=send_content,
embed=em,
files=files
destination=channel, content=send_content, embed=em, files=files
)
except (discord.Forbidden, discord.HTTPException):
return None
await self.config.custom('REPORT', guild.id, ticket_number).report.set(
{'user_id': author.id, 'report': report}
await self.config.custom("REPORT", guild.id, ticket_number).report.set(
{"user_id": author.id, "report": report}
)
return ticket_number
@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
to use it non interactively
Use without arguments for interactive reporting, or do
[p]report <text> to use it non-interactively.
"""
author = ctx.author
guild = ctx.guild
if guild is None:
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:
return
g_active = await self.config.guild(guild).active()
if not g_active:
return await author.send(
_("Reporting has not been enabled for this server")
)
return await author.send(_("Reporting has not been enabled for this server"))
if guild.id not in self.antispam:
self.antispam[guild.id] = {}
if author.id not in self.antispam[guild.id]:
self.antispam[guild.id][author.id] = AntiSpam(self.intervals)
if self.antispam[guild.id][author.id].spammy:
return await author.send(
_("You've sent a few too many of these recently. "
"Contact a server admin to resolve this, or try again "
"later.")
_(
"You've sent too many reports recently. "
"Please contact a server admin if this is important matter, "
"or please wait and try again later."
)
)
if author.id in self.user_cache:
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)
if _report:
@@ -273,42 +249,46 @@ class Reports:
else:
try:
dm = await author.send(
_("Please respond to this message with your Report."
"\nYour report should be a single message")
_(
"Please respond to this message with your Report."
"\nYour report should be a single message"
)
)
except discord.Forbidden:
await ctx.send(
_("This requires DMs enabled.")
)
self.user_cache.remove(author.id)
return
return await ctx.send(_("This requires DMs enabled."))
def pred(m):
return m.author == author and m.channel == dm.channel
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:
await author.send(
_("You took too long. Try again later.")
)
return await author.send(_("You took too long. Try again later."))
else:
val = await self.send_report(message, guild)
with contextlib.suppress(discord.Forbidden, discord.HTTPException):
if val is None:
await author.send(
_("There was an error sending your report.")
_("There was an error sending your report, please contact a server admin.")
)
else:
await author.send(
_("Your report was submitted. (Ticket #{})").format(val)
)
self.antispam[guild.id][author.id].stamp()
await author.send(_("Your report was submitted. (Ticket #{})").format(val))
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):
"""
@@ -318,18 +298,14 @@ class Reports:
return
_id = payload.message_id
t = next(filter(
lambda x: _id in x[1]['msgs'],
self.tunnel_store.items()
), None)
t = next(filter(lambda x: _id in x[1]["msgs"], self.tunnel_store.items()), None)
if t is None:
return
tun = t[1]['tun']
tun = t[1]["tun"]
if payload.user_id in [x.id for x in tun.members]:
await tun.react_close(
uid=payload.user_id,
message=_("{closer} has closed the correspondence")
uid=payload.user_id, message=_("{closer} has closed the correspondence")
)
self.tunnel_store.pop(t[0], None)
@@ -337,68 +313,62 @@ class Reports:
for k, v in self.tunnel_store.items():
topic = _("Re: ticket# {1} in {0.name}").format(*k)
# Tunnels won't forward unintended messages, this is safe
msgs = await v['tun'].communicate(message=message, topic=topic)
msgs = await v["tun"].communicate(message=message, topic=topic)
if msgs:
self.tunnel_store[k]['msgs'] = msgs
self.tunnel_store[k]["msgs"] = msgs
@checks.mod_or_permissions(manage_members=True)
@report.command(name='interact')
@report.command(name="interact")
async def response(self, ctx, ticket_number: int):
"""
opens a message tunnel between things you say in this channel
and the ticket opener's direct messages
Open a message tunnel.
This tunnel will forward things you say in this channel
to the ticket opener's direct messages.
tunnels do not persist across bot restarts
Tunnels do not persist across bot restarts.
"""
# note, mod_or_permissions is an implicit guild_only
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()
try:
user = guild.get_member(rec.get('user_id'))
user = guild.get_member(rec.get("user_id"))
except KeyError:
return await ctx.send(
_("That ticket doesn't seem to exist")
)
return await ctx.send(_("That ticket doesn't seem to exist"))
if user is None:
return await ctx.send(
_("That user isn't here anymore.")
)
return await ctx.send(_("That user isn't here anymore."))
tun = Tunnel(recipient=user, origin=ctx.channel, sender=ctx.author)
if tun is None:
return await ctx.send(
_("Either you or the user you are trying to reach already "
"has an open communication.")
_(
"Either you or the user you are trying to reach already "
"has an open communication."
)
)
big_topic = _(
"{who} opened a 2-way communication."
"{who} opened a 2-way communication "
"about ticket number {ticketnum}. Anything you say or upload here "
"(8MB file size limitation on uploads) "
"will be forwarded to them until the communication is closed.\n"
"You can close a communication at any point "
"by reacting with the X to the last message recieved. "
"\nAny message succesfully forwarded will be marked with a check."
"\nTunnels are not persistent across bot restarts."
"You can close a communication at any point by reacting with "
"the \N{NEGATIVE SQUARED CROSS MARK} to the last message recieved.\n"
"Any message succesfully forwarded will be marked with "
"\N{WHITE HEAVY CHECK MARK}.\n"
"Tunnels are not persistent across bot restarts."
)
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)
)
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:
await ctx.send(_("User has disabled DMs."))
tun.close()
await ctx.send(_("That user has DMs disabled."))
else:
self.tunnel_store[(guild, ticket_number)] = {'tun': tun, 'msgs': m}
await ctx.send(
big_topic.format(who=_("You have"), ticketnum=ticket_number)
)
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m}
await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number))

View File

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

View File

@@ -27,4 +27,4 @@ class OfflineStream(StreamsError):
class OfflineCommunity(StreamsError):
pass
pass

View File

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

View File

@@ -3,13 +3,30 @@ from redbot.core import Config, checks, commands
from redbot.core.utils.chat_formatting import pagify
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from .streamtypes import TwitchStream, HitboxStream, MixerStream, PicartoStream, TwitchCommunity, YoutubeStream
from .errors import (OfflineStream, StreamNotFound, APIError, InvalidYoutubeCredentials,
CommunityNotFound, OfflineCommunity, StreamsError, InvalidTwitchCredentials)
from .streamtypes import (
Stream,
TwitchStream,
HitboxStream,
MixerStream,
PicartoStream,
TwitchCommunity,
YoutubeStream,
)
from .errors import (
OfflineStream,
StreamNotFound,
APIError,
InvalidYoutubeCredentials,
CommunityNotFound,
OfflineCommunity,
StreamsError,
InvalidTwitchCredentials,
)
from . import streamtypes as StreamClasses
from collections import defaultdict
import asyncio
import re
from typing import Optional, List
CHECK_DELAY = 60
@@ -20,21 +37,11 @@ _ = Translator("Streams", __file__)
@cog_i18n(_)
class Streams:
global_defaults = {
"tokens": {},
"streams": [],
"communities": []
}
global_defaults = {"tokens": {}, "streams": [], "communities": []}
guild_defaults = {
"autodelete": False,
"mention_everyone": False,
"mention_here": False
}
guild_defaults = {"autodelete": False, "mention_everyone": False, "mention_here": False}
role_defaults = {
"mention": False
}
role_defaults = {"mention": False}
def __init__(self, bot: Red):
self.db = Config.get_conf(self, 26262626)
@@ -45,9 +52,11 @@ class Streams:
self.db.register_role(**self.role_defaults)
self.bot = bot
self.bot: Red = bot
self.bot.loop.create_task(self._initialize_lists())
self.streams: List[Stream] = []
self.communities: List[TwitchCommunity] = []
self.task: Optional[asyncio.Task] = None
self.yt_cid_pattern = re.compile("^UC[-_A-Za-z0-9]{21}[AQgw]$")
@@ -57,7 +66,8 @@ class Streams:
return True
return False
async def _initialize_lists(self):
async def initialize(self) -> None:
"""Should be called straight after cog instantiation."""
self.streams = await self.load_streams()
self.communities = await self.load_communities()
@@ -65,17 +75,14 @@ class Streams:
@commands.command()
async def twitch(self, ctx: commands.Context, channel_name: str):
"""Checks if a Twitch channel is streaming"""
"""Checks if a Twitch channel is live"""
token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None)
stream = TwitchStream(name=channel_name,
token=token)
stream = TwitchStream(name=channel_name, token=token)
await self.check_online(ctx, stream)
@commands.command()
async def youtube(self, ctx: commands.Context, channel_id_or_name: str):
"""
Checks if a Youtube channel is streaming
"""
"""Checks if a Youtube channel is live"""
apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None)
is_name = self.check_name_or_id(channel_id_or_name)
if is_name:
@@ -86,19 +93,19 @@ class Streams:
@commands.command()
async def hitbox(self, ctx: commands.Context, channel_name: str):
"""Checks if a Hitbox channel is streaming"""
"""Checks if a Hitbox channel is live"""
stream = HitboxStream(name=channel_name)
await self.check_online(ctx, stream)
@commands.command()
async def mixer(self, ctx: commands.Context, channel_name: str):
"""Checks if a Mixer channel is streaming"""
"""Checks if a Mixer channel is live"""
stream = MixerStream(name=channel_name)
await self.check_online(ctx, stream)
@commands.command()
async def picarto(self, ctx: commands.Context, channel_name: str):
"""Checks if a Picarto channel is streaming"""
"""Checks if a Picarto channel is live"""
stream = PicartoStream(name=channel_name)
await self.check_online(ctx, stream)
@@ -106,18 +113,25 @@ class Streams:
try:
embed = await stream.is_online()
except OfflineStream:
await ctx.send(_("The stream is offline."))
await ctx.send(_("That user is offline."))
except StreamNotFound:
await ctx.send(_("The channel doesn't seem to exist."))
await ctx.send(_("That channel doesn't seem to exist."))
except InvalidTwitchCredentials:
await ctx.send(_("The twitch token is either invalid or has not been set. "
"See `{}`.").format("{}streamset twitchtoken".format(ctx.prefix)))
await ctx.send(
_("The twitch token is either invalid or has not been set. See `{}`.").format(
"{}streamset twitchtoken".format(ctx.prefix)
)
)
except InvalidYoutubeCredentials:
await ctx.send(_("The Youtube API key is either invalid or has not been set. "
"See {}.").format("`{}streamset youtubekey`".format(ctx.prefix)))
await ctx.send(
_("Your Youtube API key is either invalid or has not been set. See {}.").format(
"`{}streamset youtubekey`".format(ctx.prefix)
)
)
except APIError:
await ctx.send(_("Something went wrong whilst trying to contact the "
"stream service's API."))
await ctx.send(
_("Something went wrong whilst trying to contact the stream service's API.")
)
else:
await ctx.send(embed=embed)
@@ -125,50 +139,46 @@ class Streams:
@commands.guild_only()
@checks.mod()
async def streamalert(self, ctx: commands.Context):
if ctx.invoked_subcommand is None:
await ctx.send_help()
pass
@streamalert.group(name="twitch")
async def _twitch(self, ctx: commands.Context):
"""Twitch stream alerts"""
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self._twitch:
await ctx.send_help()
pass
@_twitch.command(name="channel")
async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str):
"""Sets a Twitch stream alert notification in the channel"""
"""Sets a Twitch alert notification in the channel"""
await self.stream_alert(ctx, TwitchStream, channel_name.lower())
@_twitch.command(name="community")
async def twitch_alert_community(self, ctx: commands.Context, community: str):
"""Sets a Twitch stream alert notification in the channel
for the specified community."""
"""Sets an alert notification in the channel for the specified twitch community."""
await self.community_alert(ctx, TwitchCommunity, community.lower())
@streamalert.command(name="youtube")
async def youtube_alert(self, ctx: commands.Context, channel_name_or_id: str):
"""Sets a Youtube stream alert notification in the channel"""
"""Sets a Youtube alert notification in the channel"""
await self.stream_alert(ctx, YoutubeStream, channel_name_or_id)
@streamalert.command(name="hitbox")
async def hitbox_alert(self, ctx: commands.Context, channel_name: str):
"""Sets a Hitbox stream alert notification in the channel"""
"""Sets a Hitbox alert notification in the channel"""
await self.stream_alert(ctx, HitboxStream, channel_name)
@streamalert.command(name="mixer")
async def mixer_alert(self, ctx: commands.Context, channel_name: str):
"""Sets a Mixer stream alert notification in the channel"""
"""Sets a Mixer alert notification in the channel"""
await self.stream_alert(ctx, MixerStream, channel_name)
@streamalert.command(name="picarto")
async def picarto_alert(self, ctx: commands.Context, channel_name: str):
"""Sets a Picarto stream alert notification in the channel"""
"""Sets a Picarto alert notification in the channel"""
await self.stream_alert(ctx, PicartoStream, channel_name)
@streamalert.command(name="stop")
async def streamalert_stop(self, ctx: commands.Context, _all: bool=False):
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
"""Stops all stream notifications in the channel
Adding 'yes' will disable all notifications in the server"""
streams = self.streams.copy()
local_channel_ids = [c.id for c in ctx.guild.channels]
@@ -191,8 +201,9 @@ class Streams:
self.streams = streams
await self.save_streams()
msg = _("All {}'s stream alerts have been disabled."
"").format("server" if _all else "channel")
msg = _("All the alerts in the {} have been disabled.").format(
"server" if _all else "channel"
)
await ctx.send(msg)
@@ -200,7 +211,7 @@ class Streams:
async def streamalert_list(self, ctx: commands.Context):
streams_list = defaultdict(list)
guild_channels_ids = [c.id for c in ctx.guild.channels]
msg = _("Active stream alerts:\n\n")
msg = _("Active alerts:\n\n")
for stream in self.streams:
for channel_id in stream.channels:
@@ -208,7 +219,7 @@ class Streams:
streams_list[channel_id].append(stream.name.lower())
if not streams_list:
await ctx.send(_("There are no active stream alerts in this server."))
await ctx.send(_("There are no active alerts in this server."))
return
for channel_id, streams in streams_list.items():
@@ -226,23 +237,27 @@ class Streams:
if is_yt and not self.check_name_or_id(channel_name):
stream = _class(id=channel_name, token=token)
else:
stream = _class(name=channel_name,
token=token)
stream = _class(name=channel_name, token=token)
try:
exists = await self.check_exists(stream)
except InvalidTwitchCredentials:
await ctx.send(
_("The twitch token is either invalid or has not been set. "
"See {}.").format("`{}streamset twitchtoken`".format(ctx.prefix)))
_("Your twitch token is either invalid or has not been set. See {}.").format(
"`{}streamset twitchtoken`".format(ctx.prefix)
)
)
return
except InvalidYoutubeCredentials:
await ctx.send(_("The Youtube API key is either invalid or has not been set. "
"See {}.").format("`{}streamset youtubekey`".format(ctx.prefix)))
await ctx.send(
_(
"Your Youtube API key is either invalid or has not been set. See {}."
).format("`{}streamset youtubekey`".format(ctx.prefix))
)
return
except APIError:
await ctx.send(
_("Something went wrong whilst trying to contact the "
"stream service's API."))
_("Something went wrong whilst trying to contact the stream service's API.")
)
return
else:
if not exists:
@@ -260,16 +275,18 @@ class Streams:
await community.get_community_streams()
except InvalidTwitchCredentials:
await ctx.send(
_("The twitch token is either invalid or has not been set. "
"See {}.").format("`{}streamset twitchtoken`".format(ctx.prefix)))
_("The twitch token is either invalid or has not been set. See {}.").format(
"`{}streamset twitchtoken`".format(ctx.prefix)
)
)
return
except CommunityNotFound:
await ctx.send(_("That community doesn't seem to exist."))
return
except APIError:
await ctx.send(
_("Something went wrong whilst trying to contact the "
"stream service's API."))
_("Something went wrong whilst trying to contact the stream service's API.")
)
return
except OfflineCommunity:
pass
@@ -279,14 +296,12 @@ class Streams:
@commands.group()
@checks.mod()
async def streamset(self, ctx: commands.Context):
if ctx.invoked_subcommand is None:
await ctx.send_help()
pass
@streamset.command()
@checks.is_owner()
async def twitchtoken(self, ctx: commands.Context, token: str):
"""Set the Client ID for twitch.
To do this, follow these steps:
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
2. Click *Register Your Application*
@@ -294,7 +309,6 @@ class Streams:
select an Application Category of your choosing.
4. Click *Register*, and on the following page, copy the Client ID.
5. Paste the Client ID into this command. Done!
"""
await self.db.tokens.set_raw("TwitchStream", value=token)
await self.db.tokens.set_raw("TwitchCommunity", value=token)
@@ -304,14 +318,11 @@ class Streams:
@checks.is_owner()
async def youtubekey(self, ctx: commands.Context, key: str):
"""Sets the API key for Youtube.
To get one, do the following:
1. Create a project (see https://support.google.com/googleapi/answer/6251787 for details)
2. Enable the Youtube Data API v3 (see https://support.google.com/googleapi/answer/6158841 for instructions)
3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for instructions)
4. Copy your API key and paste it into this command. Done!
"""
await self.db.tokens.set_raw("YoutubeStream", value=key)
await ctx.send(_("Youtube key set."))
@@ -319,9 +330,8 @@ class Streams:
@streamset.group()
@commands.guild_only()
async def mention(self, ctx: commands.Context):
"""Sets mentions for stream alerts."""
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.mention:
await ctx.send_help()
"""Sets mentions for alerts."""
pass
@mention.command(aliases=["everyone"])
@commands.guild_only()
@@ -331,12 +341,18 @@ class Streams:
current_setting = await self.db.guild(guild).mention_everyone()
if current_setting:
await self.db.guild(guild).mention_everyone.set(False)
await ctx.send(_("{} will no longer be mentioned "
"for a stream alert.").format("@\u200beveryone"))
await ctx.send(
_("{} will no longer be mentioned when a stream or community is live").format(
"@\u200beveryone"
)
)
else:
await self.db.guild(guild).mention_everyone.set(True)
await ctx.send(_("When a stream configured for stream alerts "
"comes online, {} will be mentioned").format("@\u200beveryone"))
await ctx.send(
_("When a stream or community " "is live, {} will be mentioned.").format(
"@\u200beveryone"
)
)
@mention.command(aliases=["here"])
@commands.guild_only()
@@ -346,12 +362,14 @@ class Streams:
current_setting = await self.db.guild(guild).mention_here()
if current_setting:
await self.db.guild(guild).mention_here.set(False)
await ctx.send(_("{} will no longer be mentioned "
"for a stream alert.").format("@\u200bhere"))
await ctx.send(_("{} will no longer be mentioned for an alert.").format("@\u200bhere"))
else:
await self.db.guild(guild).mention_here.set(True)
await ctx.send(_("When a stream configured for stream alerts "
"comes online, {} will be mentioned").format("@\u200bhere"))
await ctx.send(
_("When a stream or community " "is live, {} will be mentioned.").format(
"@\u200bhere"
)
)
@mention.command()
@commands.guild_only()
@@ -363,13 +381,18 @@ class Streams:
return
if current_setting:
await self.db.role(role).mention.set(False)
await ctx.send(_("{} will no longer be mentioned "
"for a stream alert").format("@\u200b{}".format(role.name)))
await ctx.send(
_("{} will no longer be mentioned for an alert.").format(
"@\u200b{}".format(role.name)
)
)
else:
await self.db.role(role).mention.set(True)
await ctx.send(_("When a stream configured for stream alerts "
"comes online, {} will be mentioned"
"").format("@\u200b{}".format(role.name)))
await ctx.send(
_("When a stream or community " "is live, {} will be mentioned." "").format(
"@\u200b{}".format(role.name)
)
)
@streamset.command()
@commands.guild_only()
@@ -377,8 +400,7 @@ class Streams:
"""Toggles automatic deletion of notifications for streams that go offline"""
await self.db.guild(ctx.guild).autodelete.set(on_off)
if on_off:
await ctx.send("The notifications will be deleted once "
"streams go offline.")
await ctx.send("The notifications will be deleted once streams go offline.")
else:
await ctx.send("Notifications will never be deleted.")
@@ -387,14 +409,20 @@ class Streams:
stream.channels.append(ctx.channel.id)
if stream not in self.streams:
self.streams.append(stream)
await ctx.send(_("I'll send a notification in this channel when {} "
"is online.").format(stream.name))
await ctx.send(
_("I'll now send a notification in this channel when {} is live.").format(
stream.name
)
)
else:
stream.channels.remove(ctx.channel.id)
if not stream.channels:
self.streams.remove(stream)
await ctx.send(_("I won't send notifications about {} in this "
"channel anymore.").format(stream.name))
await ctx.send(
_("I won't send notifications about {} in this channel anymore.").format(
stream.name
)
)
await self.save_streams()
@@ -403,16 +431,24 @@ class Streams:
community.channels.append(ctx.channel.id)
if community not in self.communities:
self.communities.append(community)
await ctx.send(_("I'll send a notification in this channel when a "
"channel is streaming to the {} community"
"").format(community.name))
await ctx.send(
_(
"I'll send a notification in this channel when a "
"channel is live in the {} community."
""
).format(community.name)
)
else:
community.channels.remove(ctx.channel.id)
if not community.channels:
self.communities.remove(community)
await ctx.send(_("I won't send notifications about channels streaming "
"to the {} community in this channel anymore"
"").format(community.name))
await ctx.send(
_(
"I won't send notifications about channels streaming "
"in the {} community in this channel anymore."
""
).format(community.name)
)
await self.save_communities()
def get_stream(self, _class, name):
@@ -484,9 +520,9 @@ class Streams:
mention_str = await self._get_mention_str(channel.guild)
if mention_str:
content = "{}, {} is online!".format(mention_str, stream.name)
content = "{}, {} is live!".format(mention_str, stream.name)
else:
content = "{} is online!".format(stream.name)
content = "{} is live!".format(stream.name)
try:
m = await channel.send(content, embed=embed)
@@ -499,20 +535,20 @@ class Streams:
settings = self.db.guild(guild)
mentions = []
if await settings.mention_everyone():
mentions.append('@everyone')
mentions.append("@everyone")
if await settings.mention_here():
mentions.append('@here')
mentions.append("@here")
for role in guild.roles:
if await self.db.role(role).mention():
mentions.append(role.mention)
return ' '.join(mentions)
return " ".join(mentions)
async def check_communities(self):
for community in self.communities:
try:
stream_list = await community.get_community_streams()
except CommunityNotFound:
print(_("Community {} not found!").format(community.name))
print(_("The Community {} was not found!").format(community.name))
continue
except OfflineCommunity:
for message in community._messages_cache:
@@ -579,8 +615,7 @@ class Streams:
# Fast dedupe below
seen = set()
seen_add = seen.add
return [x for x in streams
if not (x.name.lower() in seen or seen_add(x.name.lower()))]
return [x for x in streams if not (x.name.lower() in seen or seen_add(x.name.lower()))]
# return streams
@@ -604,8 +639,7 @@ class Streams:
# Fast dedupe below
seen = set()
seen_add = seen.add
return [x for x in communities
if not (x.name.lower() in seen or seen_add(x.name.lower()))]
return [x for x in communities if not (x.name.lower() in seen or seen_add(x.name.lower()))]
# return communities
async def save_streams(self):
@@ -623,4 +657,5 @@ class Streams:
await self.db.communities.set(raw_communities)
def __unload(self):
self.task.cancel()
if self.task:
self.task.cancel()

View File

@@ -1,5 +1,12 @@
from .errors import StreamNotFound, APIError, OfflineStream, CommunityNotFound, OfflineCommunity, \
InvalidYoutubeCredentials, InvalidTwitchCredentials
from .errors import (
StreamNotFound,
APIError,
OfflineStream,
CommunityNotFound,
OfflineCommunity,
InvalidYoutubeCredentials,
InvalidTwitchCredentials,
)
from random import choice, sample
from string import ascii_letters
import discord
@@ -32,15 +39,12 @@ class TwitchCommunity:
self.type = self.__class__.__name__
async def get_community_id(self):
headers = {
"Accept": "application/vnd.twitchtv.v5+json",
"Client-ID": str(self._token)
}
params = {
"name": self.name
}
headers = {"Accept": "application/vnd.twitchtv.v5+json", "Client-ID": str(self._token)}
params = {"name": self.name}
async with aiohttp.ClientSession() as session:
async with session.get(TWITCH_COMMUNITIES_ENDPOINT, headers=headers, params=params) as r:
async with session.get(
TWITCH_COMMUNITIES_ENDPOINT, headers=headers, params=params
) as r:
data = await r.json()
if r.status == 200:
return data["_id"]
@@ -57,14 +61,8 @@ class TwitchCommunity:
self.id = await self.get_community_id()
except CommunityNotFound:
raise
headers = {
"Accept": "application/vnd.twitchtv.v5+json",
"Client-ID": str(self._token)
}
params = {
"community_id": self.id,
"limit": 100
}
headers = {"Accept": "application/vnd.twitchtv.v5+json", "Client-ID": str(self._token)}
params = {"community_id": self.id, "limit": 100}
url = TWITCH_BASE_URL + "/kraken/streams"
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers, params=params) as r:
@@ -82,14 +80,11 @@ class TwitchCommunity:
raise APIError()
async def make_embed(self, streams: list) -> discord.Embed:
headers = {
"Accept": "application/vnd.twitchtv.v5+json",
"Client-ID": str(self._token)
}
headers = {"Accept": "application/vnd.twitchtv.v5+json", "Client-ID": str(self._token)}
async with aiohttp.ClientSession() as session:
async with session.get(
"{}/{}".format(TWITCH_COMMUNITIES_ENDPOINT, self.id),
headers=headers) as r:
"{}/{}".format(TWITCH_COMMUNITIES_ENDPOINT, self.id), headers=headers
) as r:
data = await r.json()
avatar = data["avatar_image_url"]
@@ -102,9 +97,7 @@ class TwitchCommunity:
else:
stream_list = streams
for stream in stream_list:
name = "[{}]({})".format(
stream["channel"]["display_name"], stream["channel"]["url"]
)
name = "[{}]({})".format(stream["channel"]["display_name"], stream["channel"]["url"])
embed.add_field(name=stream["channel"]["status"], value=name, inline=False)
embed.color = 0x6441A4
@@ -128,7 +121,7 @@ class Stream:
def __init__(self, **kwargs):
self.name = kwargs.pop("name", None)
self.channels = kwargs.pop("channels", [])
#self.already_online = kwargs.pop("already_online", False)
# self.already_online = kwargs.pop("already_online", False)
self._messages_cache = kwargs.pop("_messages_cache", [])
self.type = self.__class__.__name__
@@ -167,7 +160,7 @@ class YoutubeStream(Stream):
"part": "snippet",
"channelId": self.id,
"type": "video",
"eventType": "live"
"eventType": "live",
}
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params) as r:
@@ -176,11 +169,7 @@ class YoutubeStream(Stream):
raise OfflineStream()
elif "items" in data:
vid_id = data["items"][0]["id"]["videoId"]
params = {
"key": self._token,
"id": vid_id,
"part": "snippet"
}
params = {"key": self._token, "id": vid_id, "part": "snippet"}
async with aiohttp.ClientSession() as session:
async with session.get(YOUTUBE_VIDEOS_ENDPOINT, params=params) as r:
data = await r.json()
@@ -191,7 +180,7 @@ class YoutubeStream(Stream):
video_url = "https://youtube.com/watch?v={}".format(vid_data["id"])
title = vid_data["snippet"]["title"]
thumbnail = vid_data["snippet"]["thumbnails"]["default"]["url"]
channel_title = data["snippet"]["channelTitle"]
channel_title = vid_data["snippet"]["channelTitle"]
embed = discord.Embed(title=title, url=video_url)
embed.set_author(name=channel_title)
embed.set_image(url=rnd(thumbnail))
@@ -199,17 +188,16 @@ class YoutubeStream(Stream):
return embed
async def fetch_id(self):
params = {
"key": self._token,
"forUsername": self.name,
"part": "id"
}
params = {"key": self._token, "forUsername": self.name, "part": "id"}
async with aiohttp.ClientSession() as session:
async with session.get(YOUTUBE_CHANNELS_ENDPOINT, params=params) as r:
data = await r.json()
if "error" in data and data["error"]["code"] == 400 and\
data["error"]["errors"][0]["reason"] == "keyInvalid":
if (
"error" in data
and data["error"]["code"] == 400
and data["error"]["errors"][0]["reason"] == "keyInvalid"
):
raise InvalidYoutubeCredentials()
elif "items" in data and len(data["items"]) == 0:
raise StreamNotFound()
@@ -232,19 +220,16 @@ class TwitchStream(Stream):
self.id = await self.fetch_id()
url = TWITCH_STREAMS_ENDPOINT + self.id
header = {
'Client-ID': str(self._token),
'Accept': 'application/vnd.twitchtv.v5+json'
}
header = {"Client-ID": str(self._token), "Accept": "application/vnd.twitchtv.v5+json"}
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=header) as r:
data = await r.json(encoding='utf-8')
data = await r.json(encoding="utf-8")
if r.status == 200:
if data["stream"] is None:
#self.already_online = False
# self.already_online = False
raise OfflineStream()
#self.already_online = True
# self.already_online = True
# In case of rename
self.name = data["stream"]["channel"]["name"]
return self.make_embed(data)
@@ -256,10 +241,7 @@ class TwitchStream(Stream):
raise APIError()
async def fetch_id(self):
header = {
'Client-ID': str(self._token),
'Accept': 'application/vnd.twitchtv.v5+json'
}
header = {"Client-ID": str(self._token), "Accept": "application/vnd.twitchtv.v5+json"}
url = TWITCH_ID_ENDPOINT + self.name
async with aiohttp.ClientSession() as session:
@@ -280,8 +262,7 @@ class TwitchStream(Stream):
url = channel["url"]
logo = channel["logo"]
if logo is None:
logo = ("https://static-cdn.jtvnw.net/"
"jtv_user_pictures/xarth/404_user_70x70.png")
logo = "https://static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_70x70.png"
status = channel["status"]
if not status:
status = "Untitled broadcast"
@@ -308,16 +289,16 @@ class HitboxStream(Stream):
async with aiohttp.ClientSession() as session:
async with session.get(url) as r:
#data = await r.json(encoding='utf-8')
# data = await r.json(encoding='utf-8')
data = await r.text()
data = json.loads(data, strict=False)
if "livestream" not in data:
raise StreamNotFound()
elif data["livestream"][0]["media_is_live"] == "0":
#self.already_online = False
# self.already_online = False
raise OfflineStream()
elif data["livestream"][0]["media_is_live"] == "1":
#self.already_online = True
# self.already_online = True
return self.make_embed(data)
raise APIError()
@@ -345,15 +326,15 @@ class MixerStream(Stream):
async with aiohttp.ClientSession() as session:
async with session.get(url) as r:
#data = await r.json(encoding='utf-8')
data = await r.text(encoding='utf-8')
# data = await r.json(encoding='utf-8')
data = await r.text(encoding="utf-8")
if r.status == 200:
data = json.loads(data, strict=False)
if data["online"] is True:
#self.already_online = True
# self.already_online = True
return self.make_embed(data)
else:
#self.already_online = False
# self.already_online = False
raise OfflineStream()
elif r.status == 404:
raise StreamNotFound()
@@ -361,8 +342,7 @@ class MixerStream(Stream):
raise APIError()
def make_embed(self, data):
default_avatar = ("https://mixer.com/_latest/assets/images/main/"
"avatars/default.jpg")
default_avatar = "https://mixer.com/_latest/assets/images/main/avatars/default.jpg"
user = data["user"]
url = "https://mixer.com/" + data["token"]
embed = discord.Embed(title=data["name"], url=url)
@@ -387,14 +367,14 @@ class PicartoStream(Stream):
async with aiohttp.ClientSession() as session:
async with session.get(url) as r:
data = await r.text(encoding='utf-8')
data = await r.text(encoding="utf-8")
if r.status == 200:
data = json.loads(data)
if data["online"] is True:
#self.already_online = True
# self.already_online = True
return self.make_embed(data)
else:
#self.already_online = False
# self.already_online = False
raise OfflineStream()
elif r.status == 404:
raise StreamNotFound()
@@ -402,8 +382,9 @@ class PicartoStream(Stream):
raise APIError()
def make_embed(self, data):
avatar = rnd("https://picarto.tv/user_data/usrimg/{}/dsdefault.jpg"
"".format(data["name"].lower()))
avatar = rnd(
"https://picarto.tv/user_data/usrimg/{}/dsdefault.jpg".format(data["name"].lower())
)
url = "https://picarto.tv/" + data["name"]
thumbnail = data["thumbnails"]["web"]
embed = discord.Embed(title=data["title"], url=url)
@@ -424,6 +405,5 @@ class PicartoStream(Stream):
data["adult"] = ""
embed.color = 0x4C90F3
embed.set_footer(text="{adult}Category: {category} | Tags: {tags}"
"".format(**data))
embed.set_footer(text="{adult}Category: {category} | Tags: {tags}".format(**data))
return embed

View File

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

View File

@@ -10,14 +10,16 @@ from .log import LOG
__all__ = ["TriviaSession"]
_REVEAL_MESSAGES = ("I know this one! {}!", "Easy: {}.",
"Oh really? It's {} of course.")
_FAIL_MESSAGES = ("To the next one I guess...", "Moving on...",
"I'm sure you'll know the answer of the next one.",
"\N{PENSIVE FACE} Next one.")
_REVEAL_MESSAGES = ("I know this one! {}!", "Easy: {}.", "Oh really? It's {} of course.")
_FAIL_MESSAGES = (
"To the next one I guess...",
"Moving on...",
"I'm sure you'll know the answer of the next one.",
"\N{PENSIVE FACE} Next one.",
)
class TriviaSession():
class TriviaSession:
"""Class to run a session of trivia with the user.
To run the trivia session immediately, use `TriviaSession.start` instead of
@@ -49,10 +51,7 @@ class TriviaSession():
"""
def __init__(self,
ctx,
question_list: dict,
settings: dict):
def __init__(self, ctx, question_list: dict, settings: dict):
self.ctx = ctx
list_ = list(question_list.items())
random.shuffle(list_)
@@ -128,9 +127,9 @@ class TriviaSession():
num_lists = len(list_names)
if num_lists > 2:
# at least 3 lists, join all but last with comma
msg = ", ".join(list_names[:num_lists-1])
msg = ", ".join(list_names[: num_lists - 1])
# join onto last with "and"
msg = " and ".join((msg, list_names[num_lists-1]))
msg = " and ".join((msg, list_names[num_lists - 1]))
else:
# either 1 or 2 lists, join together with "and"
msg = " and ".join(list_names)
@@ -150,10 +149,7 @@ class TriviaSession():
answers = _parse_answers(answers)
yield question, answers
async def wait_for_answer(self,
answers,
delay: float,
timeout: float):
async def wait_for_answer(self, answers, delay: float, timeout: float):
"""Wait for a correct answer, and then respond.
Scores are also updated in this method.
@@ -178,7 +174,8 @@ class TriviaSession():
"""
try:
message = await self.ctx.bot.wait_for(
"message", check=self.check_answer(answers), timeout=delay)
"message", check=self.check_answer(answers), timeout=delay
)
except asyncio.TimeoutError:
if time.time() - self._last_response >= timeout:
await self.ctx.send("Guys...? Well, I guess I'll stop then.")
@@ -194,8 +191,7 @@ class TriviaSession():
await self.ctx.send(reply)
else:
self.scores[message.author] += 1
reply = "You got it {}! **+1** to you!".format(
message.author.display_name)
reply = "You got it {}! **+1** to you!".format(message.author.display_name)
await self.ctx.send(reply)
return True
@@ -218,9 +214,9 @@ class TriviaSession():
"""
answers = tuple(s.lower() for s in answers)
def _pred(message: discord.Message):
early_exit = (message.channel != self.ctx.channel
or message.author == self.ctx.guild.me)
early_exit = message.channel != self.ctx.channel or message.author == self.ctx.guild.me
if early_exit:
return False
@@ -260,8 +256,7 @@ class TriviaSession():
"""Cancel whichever tasks this session is running."""
self._task.cancel()
channel = self.ctx.channel
LOG.debug("Force stopping trivia session; #%s in %s", channel,
channel.guild.id)
LOG.debug("Force stopping trivia session; #%s in %s", channel, channel.guild.id)
async def pay_winner(self, multiplier: float):
"""Pay the winner of this trivia session.
@@ -275,8 +270,7 @@ class TriviaSession():
paid.
"""
(winner, score) = next((tup for tup in self.scores.most_common(1)),
(None, None))
(winner, score) = next((tup for tup in self.scores.most_common(1)), (None, None))
me_ = self.ctx.guild.me
if winner is not None and winner != me_ and score > 0:
contestants = list(self.scores.keys())
@@ -285,13 +279,12 @@ class TriviaSession():
if len(contestants) >= 3:
amount = int(multiplier * score)
if amount > 0:
LOG.debug("Paying trivia winner: %d credits --> %s",
amount, str(winner))
LOG.debug("Paying trivia winner: %d credits --> %s", amount, str(winner))
await deposit_credits(winner, int(multiplier * score))
await self.ctx.send(
"Congratulations, {0}, you have received {1} credits"
" for coming first.".format(winner.display_name,
amount))
" for coming first.".format(winner.display_name, amount)
)
def _parse_answers(answers):

View File

@@ -2,7 +2,7 @@
from collections import Counter
import yaml
import discord
from discord.ext import commands
from redbot.core import commands
from redbot.ext import trivia as ext_trivia
from redbot.core import Config, checks
from redbot.core.data_manager import cog_data_path
@@ -18,6 +18,7 @@ UNIQUE_ID = 0xb3c0e453
class InvalidListError(Exception):
"""A Trivia list file is in invalid format."""
pass
@@ -26,8 +27,7 @@ class Trivia:
def __init__(self):
self.trivia_sessions = []
self.conf = Config.get_conf(
self, identifier=UNIQUE_ID, force_registration=True)
self.conf = Config.get_conf(self, identifier=UNIQUE_ID, force_registration=True)
self.conf.register_guild(
max_score=10,
@@ -36,10 +36,10 @@ class Trivia:
bot_plays=False,
reveal_answer=True,
payout_multiplier=0.0,
allow_override=True)
allow_override=True,
)
self.conf.register_member(
wins=0, games=0, total_score=0)
self.conf.register_member(wins=0, games=0, total_score=0)
@commands.group()
@commands.guild_only()
@@ -47,7 +47,6 @@ class Trivia:
async def triviaset(self, ctx: commands.Context):
"""Manage trivia settings."""
if ctx.invoked_subcommand is None:
await ctx.send_help()
settings = self.conf.guild(ctx.guild)
settings_dict = await settings.all()
msg = box(
@@ -60,7 +59,8 @@ class Trivia:
"Payout multiplier: {payout_multiplier}\n"
"Allow lists to override settings: {allow_override}"
"".format(**settings_dict),
lang="py")
lang="py",
)
await ctx.send(msg)
@triviaset.command(name="maxscore")
@@ -81,8 +81,7 @@ class Trivia:
return
settings = self.conf.guild(ctx.guild)
await settings.delay.set(seconds)
await ctx.send("Done. Maximum seconds to answer set to {}."
"".format(seconds))
await ctx.send("Done. Maximum seconds to answer set to {}.".format(seconds))
@triviaset.command(name="stopafter")
async def triviaset_stopafter(self, ctx: commands.Context, seconds: float):
@@ -92,38 +91,41 @@ class Trivia:
await ctx.send("Must be larger than the answer time limit.")
return
await settings.timeout.set(seconds)
await ctx.send("Done. Trivia sessions will now time out after {}"
" seconds of no responses.".format(seconds))
await ctx.send(
"Done. Trivia sessions will now time out after {}"
" seconds of no responses.".format(seconds)
)
@triviaset.command(name="override")
async def triviaset_allowoverride(self,
ctx: commands.Context,
enabled: bool):
async def triviaset_allowoverride(self, ctx: commands.Context, enabled: bool):
"""Allow/disallow trivia lists to override settings."""
settings = self.conf.guild(ctx.guild)
await settings.allow_override.set(enabled)
enabled = "now" if enabled else "no longer"
await ctx.send("Done. Trivia lists can {} override the trivia settings"
" for this server.".format(enabled))
await ctx.send(
"Done. Trivia lists can {} override the trivia settings"
" for this server.".format(enabled)
)
@triviaset.command(name="botplays")
async def trivaset_bot_plays(self,
ctx: commands.Context,
true_or_false: bool):
async def trivaset_bot_plays(self, ctx: commands.Context, true_or_false: bool):
"""Set whether or not the bot gains points.
If enabled, the bot will gain a point if no one guesses correctly.
"""
settings = self.conf.guild(ctx.guild)
await settings.bot_plays.set(true_or_false)
await ctx.send("Done. " + (
"I'll gain a point if users don't answer in time." if true_or_false
else "Alright, I won't embarass you at trivia anymore."))
await ctx.send(
"Done. "
+ (
"I'll gain a point if users don't answer in time."
if true_or_false
else "Alright, I won't embarass you at trivia anymore."
)
)
@triviaset.command(name="revealanswer")
async def trivaset_reveal_answer(self,
ctx: commands.Context,
true_or_false: bool):
async def trivaset_reveal_answer(self, ctx: commands.Context, true_or_false: bool):
"""Set whether or not the answer is revealed.
If enabled, the bot will reveal the answer if no one guesses correctly
@@ -131,15 +133,18 @@ class Trivia:
"""
settings = self.conf.guild(ctx.guild)
await settings.reveal_answer.set(true_or_false)
await ctx.send("Done. " + (
"I'll reveal the answer if no one knows it." if true_or_false else
"I won't reveal the answer to the questions anymore."))
await ctx.send(
"Done. "
+ (
"I'll reveal the answer if no one knows it."
if true_or_false
else "I won't reveal the answer to the questions anymore."
)
)
@triviaset.command(name="payout")
@check_global_setting_admin()
async def triviaset_payout_multiplier(self,
ctx: commands.Context,
multiplier: float):
async def triviaset_payout_multiplier(self, ctx: commands.Context, multiplier: float):
"""Set the payout multiplier.
This can be any positive decimal number. If a user wins trivia when at
@@ -155,8 +160,7 @@ class Trivia:
return
await settings.payout_multiplier.set(multiplier)
if not multiplier:
await ctx.send("Done. I will no longer reward the winner with a"
" payout.")
await ctx.send("Done. I will no longer reward the winner with a payout.")
return
await ctx.send("Done. Payout multiplier set to {}.".format(multiplier))
@@ -174,8 +178,7 @@ class Trivia:
categories = [c.lower() for c in categories]
session = self._get_trivia_session(ctx.channel)
if session is not None:
await ctx.send(
"There is already an ongoing trivia session in this channel.")
await ctx.send("There is already an ongoing trivia session in this channel.")
return
trivia_dict = {}
authors = []
@@ -185,21 +188,26 @@ class Trivia:
try:
dict_ = self.get_trivia_list(category)
except FileNotFoundError:
await ctx.send("Invalid category `{0}`. See `{1}trivia list`"
" for a list of trivia categories."
"".format(category, ctx.prefix))
await ctx.send(
"Invalid category `{0}`. See `{1}trivia list`"
" for a list of trivia categories."
"".format(category, ctx.prefix)
)
except InvalidListError:
await ctx.send("There was an error parsing the trivia list for"
" the `{}` category. It may be formatted"
" incorrectly.".format(category))
await ctx.send(
"There was an error parsing the trivia list for"
" the `{}` category. It may be formatted"
" incorrectly.".format(category)
)
else:
trivia_dict.update(dict_)
authors.append(trivia_dict.pop("AUTHOR", None))
continue
return
if not trivia_dict:
await ctx.send("The trivia list was parsed successfully, however"
" it appears to be empty!")
await ctx.send(
"The trivia list was parsed successfully, however it appears to be empty!"
)
return
settings = await self.conf.guild(ctx.guild).all()
config = trivia_dict.pop("CONFIG", None)
@@ -215,13 +223,16 @@ class Trivia:
"""Stop an ongoing trivia session."""
session = self._get_trivia_session(ctx.channel)
if session is None:
await ctx.send(
"There is no ongoing trivia session in this channel.")
await ctx.send("There is no ongoing trivia session in this channel.")
return
author = ctx.author
auth_checks = (await ctx.bot.is_owner(author), await
ctx.bot.is_mod(author), await ctx.bot.is_admin(author),
author == ctx.guild.owner, author == session.ctx.author)
auth_checks = (
await ctx.bot.is_owner(author),
await ctx.bot.is_mod(author),
await ctx.bot.is_admin(author),
author == ctx.guild.owner,
author == session.ctx.author,
)
if any(auth_checks):
await session.end_game()
session.force_stop()
@@ -234,14 +245,13 @@ class Trivia:
"""List available trivia categories."""
lists = set(p.stem for p in self._all_lists())
msg = box("**Available trivia lists**\n\n{}"
"".format(", ".join(sorted(lists))))
msg = box("**Available trivia lists**\n\n{}".format(", ".join(sorted(lists))))
if len(msg) > 1000:
await ctx.author.send(msg)
return
await ctx.send(msg)
@trivia.group(name="leaderboard", aliases=["lboard"])
@trivia.group(name="leaderboard", aliases=["lboard"], autohelp=False)
async def trivia_leaderboard(self, ctx: commands.Context):
"""Leaderboard for trivia.
@@ -256,10 +266,9 @@ class Trivia:
@trivia_leaderboard.command(name="server")
@commands.guild_only()
async def trivia_leaderboard_server(self,
ctx: commands.Context,
sort_by: str="wins",
top: int=10):
async def trivia_leaderboard_server(
self, ctx: commands.Context, sort_by: str = "wins", top: int = 10
):
"""Leaderboard for this server.
<sort_by> can be any of the following fields:
@@ -271,9 +280,11 @@ class Trivia:
"""
key = self._get_sort_key(sort_by)
if key is None:
await ctx.send("Unknown field `{}`, see `{}help trivia "
"leaderboard server` for valid fields to sort by."
"".format(sort_by, ctx.prefix))
await ctx.send(
"Unknown field `{}`, see `{}help trivia "
"leaderboard server` for valid fields to sort by."
"".format(sort_by, ctx.prefix)
)
return
guild = ctx.guild
data = await self.conf.all_members(guild)
@@ -282,10 +293,9 @@ class Trivia:
await self.send_leaderboard(ctx, data, key, top)
@trivia_leaderboard.command(name="global")
async def trivia_leaderboard_global(self,
ctx: commands.Context,
sort_by: str="wins",
top: int=10):
async def trivia_leaderboard_global(
self, ctx: commands.Context, sort_by: str = "wins", top: int = 10
):
"""Global trivia leaderboard.
<sort_by> can be any of the following fields:
@@ -298,9 +308,11 @@ class Trivia:
"""
key = self._get_sort_key(sort_by)
if key is None:
await ctx.send("Unknown field `{}`, see `{}help trivia "
"leaderboard global` for valid fields to sort by."
"".format(sort_by, ctx.prefix))
await ctx.send(
"Unknown field `{}`, see `{}help trivia "
"leaderboard global` for valid fields to sort by."
"".format(sort_by, ctx.prefix)
)
return
data = await self.conf.all_members()
collated_data = {}
@@ -327,11 +339,7 @@ class Trivia:
elif key in ("total", "score", "answers", "correct"):
return "total_score"
async def send_leaderboard(self,
ctx: commands.Context,
data: dict,
key: str,
top: int):
async def send_leaderboard(self, ctx: commands.Context, data: dict, key: str, top: int):
"""Send the leaderboard from the given data.
Parameters
@@ -374,7 +382,7 @@ class Trivia:
try:
priority.remove(key)
except ValueError:
raise ValueError("{} is not a valid key".format(key))
raise ValueError("{} is not a valid key.".format(key))
# Put key last in reverse priority
priority.append(key)
items = data.items()
@@ -382,23 +390,34 @@ class Trivia:
items = sorted(items, key=lambda t: t[1][key], reverse=True)
max_name_len = max(map(lambda m: len(str(m)), data.keys()))
# Headers
headers = ("Rank", "Member{}".format(" " * (max_name_len - 6)), "Wins",
"Games Played", "Total Score", "Average Score")
headers = (
"Rank",
"Member{}".format(" " * (max_name_len - 6)),
"Wins",
"Games Played",
"Total Score",
"Average Score",
)
lines = [" | ".join(headers)]
# Header underlines
lines.append(" | ".join(("-" * len(h) for h in headers)))
for rank, tup in enumerate(items, 1):
member, m_data = tup
# Align fields to header width
fields = tuple(map(str, (rank,
member,
m_data["wins"],
m_data["games"],
m_data["total_score"],
round(m_data["average_score"], 2))))
padding = [
" " * (len(h) - len(f)) for h, f in zip(headers, fields)
]
fields = tuple(
map(
str,
(
rank,
member,
m_data["wins"],
m_data["games"],
m_data["total_score"],
round(m_data["average_score"], 2),
),
)
)
padding = [" " * (len(h) - len(f)) for h, f in zip(headers, fields)]
fields = tuple(f + padding[i] for i, f in enumerate(fields))
lines.append(" | ".join(fields).format(member=member, **m_data))
if rank == top:
@@ -418,8 +437,7 @@ class Trivia:
"""
channel = session.ctx.channel
LOG.debug("Ending trivia session; #%s in %s", channel,
channel.guild.id)
LOG.debug("Ending trivia session; #%s in %s", channel, channel.guild.id)
if session in self.trivia_sessions:
self.trivia_sessions.remove(session)
if session.scores:
@@ -462,25 +480,23 @@ class Trivia:
try:
path = next(p for p in self._all_lists() if p.stem == category)
except StopIteration:
raise FileNotFoundError("Could not find the `{}` category"
"".format(category))
raise FileNotFoundError("Could not find the `{}` category.".format(category))
with path.open(encoding='utf-8') as file:
with path.open(encoding="utf-8") as file:
try:
dict_ = yaml.load(file)
except yaml.error.YAMLError as exc:
raise InvalidListError("YAML parsing failed") from exc
raise InvalidListError("YAML parsing failed.") from exc
else:
return dict_
def _get_trivia_session(self,
channel: discord.TextChannel) -> TriviaSession:
return next((session for session in self.trivia_sessions
if session.ctx.channel == channel), None)
def _get_trivia_session(self, channel: discord.TextChannel) -> TriviaSession:
return next(
(session for session in self.trivia_sessions if session.ctx.channel == channel), None
)
def _all_lists(self):
personal_lists = tuple(p.resolve()
for p in cog_data_path(self).glob("*.yaml"))
personal_lists = tuple(p.resolve() for p in cog_data_path(self).glob("*.yaml"))
return personal_lists + tuple(ext_trivia.lists())

View File

@@ -9,7 +9,9 @@ from redbot.core.i18n import Translator
_ = Translator("Warnings", __file__)
async def warning_points_add_check(config: Config, ctx: commands.Context, user: discord.Member, points: int):
async def warning_points_add_check(
config: Config, ctx: commands.Context, user: discord.Member, points: int
):
"""Handles any action that needs to be taken or not based on the points"""
guild = ctx.guild
guild_settings = config.guild(guild)
@@ -20,11 +22,13 @@ async def warning_points_add_check(config: Config, ctx: commands.Context, user:
act = a
else:
break
if act: # some action needs to be taken
if act and act["exceed_command"] is not None: # some action needs to be taken
await create_and_invoke_context(ctx, act["exceed_command"], user)
async def warning_points_remove_check(config: Config, ctx: commands.Context, user: discord.Member, points: int):
async def warning_points_remove_check(
config: Config, ctx: commands.Context, user: discord.Member, points: int
):
guild = ctx.guild
guild_settings = config.guild(guild)
act = {}
@@ -34,11 +38,13 @@ async def warning_points_remove_check(config: Config, ctx: commands.Context, use
act = a
else:
break
if act: # some action needs to be taken
if act and act["drop_command"] is not None: # some action needs to be taken
await create_and_invoke_context(ctx, act["drop_command"], user)
async def create_and_invoke_context(realctx: commands.Context, command_str: str, user: discord.Member):
async def create_and_invoke_context(
realctx: commands.Context, command_str: str, user: discord.Member
):
m = copy(realctx.message)
m.content = command_str.format(user=user.mention, prefix=realctx.prefix)
fctx = await realctx.bot.get_context(m, cls=commands.Context)
@@ -54,7 +60,7 @@ def get_command_from_input(bot, userinput: str):
while com is None:
com = bot.get_command(userinput)
if com is None:
userinput = ' '.join(userinput.split(' ')[:-1])
userinput = " ".join(userinput.split(" ")[:-1])
if len(userinput) == 0:
break
if com is None:
@@ -63,8 +69,10 @@ def get_command_from_input(bot, userinput: str):
check_str = inspect.getsource(checks.is_owner)
if any(inspect.getsource(x) in check_str for x in com.checks):
# command the user specified has the is_owner check
return None, _("That command requires bot owner. I can't "
"allow you to use that for an action")
return (
None,
_("That command requires bot owner. I can't allow you to use that for an action"),
)
return "{prefix}" + orig, None
@@ -72,13 +80,16 @@ async def get_command_for_exceeded_points(ctx: commands.Context):
"""Gets the command to be executed when the user is at or exceeding
the points threshold for the action"""
await ctx.send(
_("Enter the command to be run when the user exceeds the points for "
"this action to occur.\nEnter it exactly as you would if you were "
"actually trying to run the command, except don't put a prefix and "
"use {user} in place of any user/member arguments\n\n"
"WARNING: The command entered will be run without regard to checks or cooldowns. "
"Commands requiring bot owner are not allowed for security reasons.\n\n"
"Please wait 15 seconds before entering your response.")
_(
"Enter the command to be run when the user **exceeds the points for "
"this action to occur.**\n**If you do not wish to have a command run, enter** "
"`none`.\n\nEnter it exactly as you would if you were "
"actually trying to run the command, except don't put a prefix and "
"use `{user}` in place of any user/member arguments\n\n"
"WARNING: The command entered will be run without regard to checks or cooldowns. "
"Commands requiring bot owner are not allowed for security reasons.\n\n"
"Please wait 15 seconds before entering your response."
)
)
await asyncio.sleep(15)
@@ -90,8 +101,10 @@ async def get_command_for_exceeded_points(ctx: commands.Context):
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
except asyncio.TimeoutError:
await ctx.send(_("Ok then."))
return None
else:
if msg.content == "none":
return None
command, m = get_command_from_input(ctx.bot, msg.content)
if command is None:
@@ -110,15 +123,18 @@ async def get_command_for_dropping_points(ctx: commands.Context):
when the user exceeded the threshold
"""
await ctx.send(
_("Enter the command to be run when the user returns to a value below "
"the points for this action to occur. Please note that this is "
"intended to be used for reversal of the action taken when the user "
"exceeded the action's point value\nEnter it exactly as you would "
"if you were actually trying to run the command, except don't put a prefix "
"and use {user} in place of any user/member arguments\n\n"
"WARNING: The command entered will be run without regard to checks or cooldowns. "
"Commands requiring bot owner are not allowed for security reasons.\n\n"
"Please wait 15 seconds before entering your response.")
_(
"Enter the command to be run when the user **returns to a value below "
"the points for this action to occur.** Please note that this is "
"intended to be used for reversal of the action taken when the user "
"exceeded the action's point value.\n**If you do not wish to have a command run "
"on dropping points, enter** `none`.\n\nEnter it exactly as you would "
"if you were actually trying to run the command, except don't put a prefix "
"and use `{user}` in place of any user/member arguments\n\n"
"WARNING: The command entered will be run without regard to checks or cooldowns. "
"Commands requiring bot owner are not allowed for security reasons.\n\n"
"Please wait 15 seconds before entering your response."
)
)
await asyncio.sleep(15)
@@ -130,9 +146,10 @@ async def get_command_for_dropping_points(ctx: commands.Context):
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
except asyncio.TimeoutError:
await ctx.send(_("Ok then."))
return None
else:
if msg.content == "none":
return None
command, m = get_command_from_input(ctx.bot, msg.content)
if command is None:
await ctx.send(m)

View File

@@ -1,15 +1,10 @@
import subprocess
TO_TRANSLATE = [
'../warnings.py',
'../helpers.py'
]
TO_TRANSLATE = ["../warnings.py", "../helpers.py"]
def regen_messages():
subprocess.run(
['pygettext', '-n'] + TO_TRANSLATE
)
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
if __name__ == "__main__":

View File

@@ -3,13 +3,18 @@ from collections import namedtuple
import discord
import asyncio
from redbot.cogs.warnings.helpers import warning_points_add_check, get_command_for_exceeded_points, \
get_command_for_dropping_points, warning_points_remove_check
from redbot.cogs.warnings.helpers import (
warning_points_add_check,
get_command_for_exceeded_points,
get_command_for_dropping_points,
warning_points_remove_check,
)
from redbot.core import Config, modlog, checks, commands
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.mod import is_admin_or_superior
from redbot.core.utils.chat_formatting import warning, pagify
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
_ = Translator("Warnings", __file__)
@@ -18,17 +23,9 @@ _ = Translator("Warnings", __file__)
class Warnings:
"""A warning system for Red"""
default_guild = {
"actions": [],
"reasons": {},
"allow_custom_reasons": False
}
default_guild = {"actions": [], "reasons": {}, "allow_custom_reasons": False}
default_member = {
"total_points": 0,
"status": "",
"warnings": {}
}
default_member = {"total_points": 0, "status": "", "warnings": {}}
def __init__(self, bot: Red):
self.config = Config.get_conf(self, identifier=5757575755)
@@ -41,9 +38,7 @@ class Warnings:
@staticmethod
async def register_warningtype():
try:
await modlog.register_casetype(
"warning", True, "\N{WARNING SIGN}", "Warning", None
)
await modlog.register_casetype("warning", True, "\N{WARNING SIGN}", "Warning", None)
except RuntimeError:
pass
@@ -52,17 +47,16 @@ class Warnings:
@checks.guildowner_or_permissions(administrator=True)
async def warningset(self, ctx: commands.Context):
"""Warning settings"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
pass
@warningset.command()
@commands.guild_only()
async def allowcustomreasons(self, ctx: commands.Context, allowed: bool):
"""Allow or disallow custom reasons for a warning"""
"""Enable or Disable custom reasons for a warning"""
guild = ctx.guild
await self.config.guild(guild).allow_custom_reasons.set(allowed)
await ctx.send(
_("Custom reasons have been {}").format(_("enabled") if allowed else _("disabled"))
_("Custom reasons have been {}.").format(_("enabled") if allowed else _("disabled"))
)
@commands.group()
@@ -70,42 +64,25 @@ class Warnings:
@checks.guildowner_or_permissions(administrator=True)
async def warnaction(self, ctx: commands.Context):
"""Action management"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
pass
@warnaction.command(name="add")
@commands.guild_only()
async def action_add(self, ctx: commands.Context, name: str, points: int):
"""Create an action to be taken at a specified point count
Duplicate action names are not allowed"""
Duplicate action names are not allowed
"""
guild = ctx.guild
await ctx.send("Would you like to enter commands to be run? (y/n)")
exceed_command = await get_command_for_exceeded_points(ctx)
drop_command = await get_command_for_dropping_points(ctx)
def same_author_check(m):
return m.author == ctx.author
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
except asyncio.TimeoutError:
await ctx.send(_("Ok then"))
return
if msg.content.lower() == "y":
exceed_command = await get_command_for_exceeded_points(ctx)
if exceed_command is None:
return
drop_command = await get_command_for_dropping_points(ctx)
if drop_command is None:
return
else:
exceed_command = None
drop_command = None
to_add = {
"action_name": name,
"points": points,
"exceed_command": exceed_command,
"drop_command": drop_command
"drop_command": drop_command,
}
# Have all details for the action, now save the action
@@ -120,7 +97,7 @@ class Warnings:
# Sort in descending order by point count for ease in
# finding the highest possible action to take
registered_actions.sort(key=lambda a: a["points"], reverse=True)
await ctx.tick()
await ctx.send(_("Action {name} has been added.").format(name=name))
@warnaction.command(name="del")
@commands.guild_only()
@@ -138,17 +115,14 @@ class Warnings:
registered_actions.remove(to_remove)
await ctx.tick()
else:
await ctx.send(
_("No action named {} exists!").format(action_name)
)
await ctx.send(_("No action named {} exists!").format(action_name))
@commands.group()
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
async def warnreason(self, ctx: commands.Context):
"""Add reasons for warnings"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
pass
@warnreason.command(name="add")
@commands.guild_only()
@@ -159,20 +133,15 @@ class Warnings:
if name.lower() == "custom":
await ctx.send("That cannot be used as a reason name!")
return
to_add = {
"points": points,
"description": description
}
completed = {
name.lower(): to_add
}
to_add = {"points": points, "description": description}
completed = {name.lower(): to_add}
guild_settings = self.config.guild(guild)
async with guild_settings.reasons() as registered_reasons:
registered_reasons.update(completed)
await ctx.send(_("That reason has been registered"))
await ctx.send(_("That reason has been registered."))
@warnreason.command(name="del")
@commands.guild_only()
@@ -184,7 +153,7 @@ class Warnings:
if registered_reasons.pop(reason_name.lower(), None):
await ctx.tick()
else:
await ctx.send(_("That is not a registered reason name"))
await ctx.send(_("That is not a registered reason name."))
@commands.command()
@commands.guild_only()
@@ -196,13 +165,20 @@ class Warnings:
msg_list = []
async with guild_settings.reasons() as registered_reasons:
for r, v in registered_reasons.items():
msg_list.append(
"Name: {}\nPoints: {}\nDescription: {}".format(
r, v["points"], v["description"]
if ctx.embed_requested():
em = discord.Embed(
title=_("Reason: {name}").format(name=r), description=v["description"]
)
em.add_field(name=_("Points"), value=str(v["points"]))
msg_list.append(em)
else:
msg_list.append(
"Name: {}\nPoints: {}\nDescription: {}".format(
r, v["points"], v["description"]
)
)
)
if msg_list:
await ctx.send_interactive(msg_list)
await menu(ctx, msg_list, DEFAULT_CONTROLS)
else:
await ctx.send(_("There are no reasons configured!"))
@@ -216,15 +192,21 @@ class Warnings:
msg_list = []
async with guild_settings.actions() as registered_actions:
for r in registered_actions:
msg_list.append(
"Name: {}\nPoints: {}\nExceed command: {}\n"
"Drop command: {}".format(
r["action_name"], r["points"], r["exceed_command"],
r["drop_command"]
if await ctx.embed_requested():
em = discord.Embed(title=_("Action: {name}").format(name=r["action_name"]))
em.add_field(name=_("Points"), value="{}".format(r["points"]), inline=False)
em.add_field(name=_("Exceed command"), value=r["exceed_command"], inline=False)
em.add_field(name=_("Drop command"), value=r["drop_command"], inline=False)
msg_list.append(em)
else:
msg_list.append(
"Name: {}\nPoints: {}\nExceed command: {}\n"
"Drop command: {}".format(
r["action_name"], r["points"], r["exceed_command"], r["drop_command"]
)
)
)
if msg_list:
await ctx.send_interactive(msg_list)
await menu(ctx, msg_list, DEFAULT_CONTROLS)
else:
await ctx.send(_("There are no actions configured!"))
@@ -233,18 +215,20 @@ class Warnings:
@checks.admin_or_permissions(ban_members=True)
async def warn(self, ctx: commands.Context, user: discord.Member, reason: str):
"""Warn the user for the specified reason
Reason must be a registered reason, or custom if custom reasons are allowed"""
reason_type = {}
Reason must be a registered reason, or "custom" if custom reasons are allowed
"""
if user == ctx.author:
await ctx.send(_("You cannot warn yourself."))
return
if reason.lower() == "custom":
custom_allowed = await self.config.guild(ctx.guild).allow_custom_reasons()
if not custom_allowed:
await ctx.send(
_(
"Custom reasons are not allowed! Please see {} for "
"a complete list of valid reasons"
).format(
"`{}reasonlist`".format(ctx.prefix)
)
"a complete list of valid reasons."
).format("`{}reasonlist`".format(ctx.prefix))
)
return
reason_type = await self.custom_warning_reason(ctx)
@@ -253,6 +237,7 @@ class Warnings:
async with guild_settings.reasons() as registered_reasons:
if reason.lower() not in registered_reasons:
await ctx.send(_("That is not a registered reason!"))
return
else:
reason_type = registered_reasons[reason.lower()]
@@ -262,7 +247,7 @@ class Warnings:
str(ctx.message.id): {
"points": reason_type["points"],
"description": reason_type["description"],
"mod": ctx.author.id
"mod": ctx.author.id,
}
}
async with member_settings.warnings() as user_warnings:
@@ -271,24 +256,43 @@ class Warnings:
await member_settings.total_points.set(current_point_count)
await warning_points_add_check(self.config, ctx, user, current_point_count)
await ctx.tick()
try:
em = discord.Embed(
title=_("Warning from {mod_name}#{mod_discrim}").format(
mod_name=ctx.author.display_name, mod_discrim=ctx.author.discriminator
),
description=reason_type["description"],
)
em.add_field(name=_("Points"), value=str(reason_type["points"]))
await user.send(
_("You have received a warning in {guild_name}.").format(
guild_name=ctx.guild.name
),
embed=em,
)
except discord.HTTPException:
pass
await ctx.send(
_("User {user_name}#{user_discrim} has been warned.").format(
user_name=user.display_name, user_discrim=user.discriminator
)
)
@commands.command()
@commands.guild_only()
async def warnings(self, ctx: commands.Context, userid: int=None):
async def warnings(self, ctx: commands.Context, userid: int = None):
"""Show warnings for the specified user.
If userid is None, show warnings for the person running the command
Note that showing warnings for users other than yourself requires
appropriate permissions"""
appropriate permissions
"""
if userid is None:
user = ctx.author
else:
if not await is_admin_or_superior(self.bot, ctx.author):
await ctx.send(
warning(
_("You are not allowed to check "
"warnings for other users!")
)
warning(_("You are not allowed to check warnings for other users!"))
)
return
else:
@@ -305,28 +309,23 @@ class Warnings:
mod = ctx.guild.get_member(user_warnings[key]["mod"])
if mod is None:
mod = discord.utils.get(
self.bot.get_all_members(),
id=user_warnings[key]["mod"]
self.bot.get_all_members(), id=user_warnings[key]["mod"]
)
if mod is None:
mod = await self.bot.get_user_info(
user_warnings[key]["mod"]
)
mod = await self.bot.get_user_info(user_warnings[key]["mod"])
msg += "{} point warning {} issued by {} for {}\n".format(
user_warnings[key]["points"],
key,
mod,
user_warnings[key]["description"]
user_warnings[key]["points"], key, mod, user_warnings[key]["description"]
)
await ctx.send_interactive(
pagify(msg), box_lang="Warnings for {}".format(user)
)
await ctx.send_interactive(pagify(msg), box_lang="Warnings for {}".format(user))
@commands.command()
@commands.guild_only()
@checks.admin_or_permissions(ban_members=True)
async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str):
"""Removes the specified warning from the user specified"""
if user_id == ctx.author.id:
await ctx.send(_("You cannot remove warnings from yourself."))
return
guild = ctx.guild
member = guild.get_member(user_id)
if member is None: # no longer in guild, but need a "member" object
@@ -348,10 +347,7 @@ class Warnings:
@staticmethod
async def custom_warning_reason(ctx: commands.Context):
"""Handles getting description and points for custom reasons"""
to_add = {
"points": 0,
"description": ""
}
to_add = {"points": 0, "description": ""}
def same_author_check(m):
return m.author == ctx.author
@@ -360,7 +356,7 @@ class Warnings:
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
except asyncio.TimeoutError:
await ctx.send(_("Ok then"))
await ctx.send(_("Ok then."))
return
try:
int(msg.content)
@@ -373,11 +369,11 @@ class Warnings:
return
to_add["points"] = int(msg.content)
await ctx.send(_("Enter a description for this reason"))
await ctx.send(_("Enter a description for this reason."))
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
except asyncio.TimeoutError:
await ctx.send(_("Ok then"))
await ctx.send(_("Ok then."))
return
to_add["description"] = msg.content
return to_add

View File

@@ -5,15 +5,13 @@ __all__ = ["Config", "__version__"]
class VersionInfo:
def __init__(self, major, minor, micro, releaselevel, serial):
self._levels = ['alpha', 'beta', 'final']
self._levels = ["alpha", "beta", "final"]
self.major = major
self.minor = minor
self.micro = micro
if releaselevel not in self._levels:
raise TypeError("'releaselevel' must be one of: {}".format(
', '.join(self._levels)
))
raise TypeError("'releaselevel' must be one of: {}".format(", ".join(self._levels)))
self.releaselevel = releaselevel
self.serial = serial
@@ -21,8 +19,13 @@ class VersionInfo:
def __lt__(self, other):
my_index = self._levels.index(self.releaselevel)
other_index = self._levels.index(other.releaselevel)
return (self.major, self.minor, self.micro, my_index, self.serial) < \
(other.major, other.minor, other.micro, other_index, other.serial)
return (self.major, self.minor, self.micro, my_index, self.serial) < (
other.major,
other.minor,
other.micro,
other_index,
other.serial,
)
def __repr__(self):
return "VersionInfo(major={}, minor={}, micro={}, releaselevel={}, serial={})".format(
@@ -32,5 +35,6 @@ class VersionInfo:
def to_json(self):
return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
__version__ = "3.0.0b14"
version_info = VersionInfo(3, 0, 0, 'beta', 14)
__version__ = "3.0.0b17"
version_info = VersionInfo(3, 0, 0, "beta", 17)

View File

@@ -6,29 +6,36 @@ import discord
from redbot.core import Config
__all__ = ["Account", "get_balance", "set_balance", "withdraw_credits", "deposit_credits",
"can_spend", "transfer_credits", "wipe_bank", "get_account", "is_global",
"set_global", "get_bank_name", "set_bank_name", "get_currency_name",
"set_currency_name", "get_default_balance", "set_default_balance"]
__all__ = [
"Account",
"get_balance",
"set_balance",
"withdraw_credits",
"deposit_credits",
"can_spend",
"transfer_credits",
"wipe_bank",
"get_account",
"is_global",
"set_global",
"get_bank_name",
"set_bank_name",
"get_currency_name",
"set_currency_name",
"get_default_balance",
"set_default_balance",
]
_DEFAULT_GLOBAL = {
"is_global": False,
"bank_name": "Twentysix bank",
"currency": "credits",
"default_balance": 100
"default_balance": 100,
}
_DEFAULT_GUILD = {
"bank_name": "Twentysix bank",
"currency": "credits",
"default_balance": 100
}
_DEFAULT_GUILD = {"bank_name": "Twentysix bank", "currency": "credits", "default_balance": 100}
_DEFAULT_MEMBER = {
"name": "",
"balance": 0,
"created_at": 0
}
_DEFAULT_MEMBER = {"name": "", "balance": 0, "created_at": 0}
_DEFAULT_USER = _DEFAULT_MEMBER
@@ -50,9 +57,9 @@ def _register_defaults():
_conf.register_member(**_DEFAULT_MEMBER)
_conf.register_user(**_DEFAULT_USER)
if not os.environ.get('BUILDING_DOCS'):
_conf = Config.get_conf(
None, 384734293238749, cog_name="Bank", force_registration=True)
if not os.environ.get("BUILDING_DOCS"):
_conf = Config.get_conf(None, 384734293238749, cog_name="Bank", force_registration=True)
_register_defaults()
@@ -285,7 +292,7 @@ async def wipe_bank():
await _conf.clear_all_members()
async def get_leaderboard(positions: int=None, guild: discord.Guild=None) -> List[tuple]:
async def get_leaderboard(positions: int = None, guild: discord.Guild = None) -> List[tuple]:
"""
Gets the bank's leaderboard
@@ -319,14 +326,16 @@ async def get_leaderboard(positions: int=None, guild: discord.Guild=None) -> Lis
if guild is None:
raise TypeError("Expected a guild, got NoneType object instead!")
raw_accounts = await _conf.all_members(guild)
sorted_acc = sorted(raw_accounts.items(), key=lambda x: x[1]['balance'], reverse=True)
sorted_acc = sorted(raw_accounts.items(), key=lambda x: x[1]["balance"], reverse=True)
if positions is None:
return sorted_acc
else:
return sorted_acc[:positions]
async def get_leaderboard_position(member: Union[discord.User, discord.Member]) -> Union[int, None]:
async def get_leaderboard_position(
member: Union[discord.User, discord.Member]
) -> Union[int, None]:
"""
Get the leaderboard position for the specified user
@@ -387,13 +396,13 @@ async def get_account(member: Union[discord.Member, discord.User]) -> Account:
if acc_data == {}:
acc_data = default
acc_data['name'] = member.display_name
acc_data["name"] = member.display_name
try:
acc_data['balance'] = await get_default_balance(member.guild)
acc_data["balance"] = await get_default_balance(member.guild)
except AttributeError:
acc_data['balance'] = await get_default_balance()
acc_data["balance"] = await get_default_balance()
acc_data['created_at'] = _decode_time(acc_data['created_at'])
acc_data["created_at"] = _decode_time(acc_data["created_at"])
return Account(**acc_data)
@@ -444,7 +453,7 @@ async def set_global(global_: bool) -> bool:
return global_
async def get_bank_name(guild: discord.Guild=None) -> str:
async def get_bank_name(guild: discord.Guild = None) -> str:
"""Get the current bank name.
Parameters
@@ -472,7 +481,7 @@ async def get_bank_name(guild: discord.Guild=None) -> str:
raise RuntimeError("Guild parameter is required and missing.")
async def set_bank_name(name: str, guild: discord.Guild=None) -> str:
async def set_bank_name(name: str, guild: discord.Guild = None) -> str:
"""Set the bank name.
Parameters
@@ -499,12 +508,11 @@ async def set_bank_name(name: str, guild: discord.Guild=None) -> str:
elif guild is not None:
await _conf.guild(guild).bank_name.set(name)
else:
raise RuntimeError("Guild must be provided if setting the name of a guild"
"-specific bank.")
raise RuntimeError("Guild must be provided if setting the name of a guild-specific bank.")
return name
async def get_currency_name(guild: discord.Guild=None) -> str:
async def get_currency_name(guild: discord.Guild = None) -> str:
"""Get the currency name of the bank.
Parameters
@@ -532,7 +540,7 @@ async def get_currency_name(guild: discord.Guild=None) -> str:
raise RuntimeError("Guild must be provided.")
async def set_currency_name(name: str, guild: discord.Guild=None) -> str:
async def set_currency_name(name: str, guild: discord.Guild = None) -> str:
"""Set the currency name for the bank.
Parameters
@@ -559,12 +567,13 @@ async def set_currency_name(name: str, guild: discord.Guild=None) -> str:
elif guild is not None:
await _conf.guild(guild).currency.set(name)
else:
raise RuntimeError("Guild must be provided if setting the currency"
" name of a guild-specific bank.")
raise RuntimeError(
"Guild must be provided if setting the currency name of a guild-specific bank."
)
return name
async def get_default_balance(guild: discord.Guild=None) -> int:
async def get_default_balance(guild: discord.Guild = None) -> int:
"""Get the current default balance amount.
Parameters
@@ -592,7 +601,7 @@ async def get_default_balance(guild: discord.Guild=None) -> int:
raise RuntimeError("Guild is missing and required!")
async def set_default_balance(amount: int, guild: discord.Guild=None) -> int:
async def set_default_balance(amount: int, guild: discord.Guild = None) -> int:
"""Set the default balance amount.
Parameters

View File

@@ -14,32 +14,17 @@ from discord.ext.commands import when_mentioned_or
# This supresses the PyNaCl warning that isn't relevant here
from discord.voice_client import VoiceClient
VoiceClient.warn_nacl = False
from .cog_manager import CogManager
from . import (
Config,
i18n,
commands,
rpc
)
from . import Config, i18n, commands
from .rpc import RPCMixin
from .help_formatter import Help, help as help_
from .sentry import SentryManager
from .utils import TYPE_CHECKING
if TYPE_CHECKING:
from aiohttp_json_rpc import JsonRpc
# noinspection PyUnresolvedReferences
class RpcMethodMixin:
async def rpc__cogs(self, request):
return list(self.cogs.keys())
async def rpc__extensions(self, request):
return list(self.extensions.keys())
class RedBase(BotBase, RpcMethodMixin):
class RedBase(BotBase, RPCMixin):
"""Mixin for the main bot class.
This exists because `Red` inherits from `discord.AutoShardedClient`, which
@@ -48,7 +33,8 @@ class RedBase(BotBase, RpcMethodMixin):
Selfbots should inherit from this mixin along with `discord.Client`.
"""
def __init__(self, cli_flags, bot_dir: Path=Path.cwd(), **kwargs):
def __init__(self, *args, cli_flags=None, bot_dir: Path = Path.cwd(), **kwargs):
self._shutdown_mode = ExitCodes.CRITICAL
self.db = Config.get_core_conf(force_registration=True)
self._co_owners = cli_flags.co_owner
@@ -62,8 +48,13 @@ class RedBase(BotBase, RpcMethodMixin):
whitelist=[],
blacklist=[],
enable_sentry=None,
locale='en',
embeds=True
locale="en",
embeds=True,
color=15158332,
fuzzy=False,
help__page_char_limit=1000,
help__max_pages_in_guild=2,
help__tagline="",
)
self.db.register_guild(
@@ -72,12 +63,12 @@ class RedBase(BotBase, RpcMethodMixin):
blacklist=[],
admin_role=None,
mod_role=None,
embeds=None
embeds=None,
use_bot_color=False,
fuzzy=False,
)
self.db.register_user(
embeds=None
)
self.db.register_user(embeds=None)
async def prefix_manager(bot, message):
if not cli_flags.prefix:
@@ -88,9 +79,11 @@ class RedBase(BotBase, RpcMethodMixin):
return global_prefix
server_prefix = await bot.db.guild(message.guild).prefix()
if cli_flags.mentionable:
return when_mentioned_or(*server_prefix)(bot, message) \
if server_prefix else \
when_mentioned_or(*global_prefix)(bot, message)
return (
when_mentioned_or(*server_prefix)(bot, message)
if server_prefix
else when_mentioned_or(*global_prefix)(bot, message)
)
else:
return server_prefix if server_prefix else global_prefix
@@ -104,18 +97,20 @@ class RedBase(BotBase, RpcMethodMixin):
loop = asyncio.get_event_loop()
loop.run_until_complete(self._dict_abuse(kwargs))
if "command_not_found" not in kwargs:
kwargs["command_not_found"] = "Command {} not found.\n{}"
self.counter = Counter()
self.uptime = None
self.color = discord.Embed.Empty # This is needed or color ends up 0x000000
self.main_dir = bot_dir
self.cog_mgr = CogManager(paths=(str(self.main_dir / 'cogs'),))
self.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),))
self.register_rpc_methods()
super().__init__(*args, formatter=Help(), **kwargs)
super().__init__(formatter=Help(), **kwargs)
self.remove_command('help')
self.remove_command("help")
self.add_command(help_)
@@ -124,7 +119,7 @@ class RedBase(BotBase, RpcMethodMixin):
def enable_sentry(self):
"""Enable Sentry logging for Red."""
if self._sentry_mgr is None:
sentry_log = logging.getLogger('red.sentry')
sentry_log = logging.getLogger("red.sentry")
sentry_log.setLevel(logging.WARNING)
self._sentry_mgr = SentryManager(sentry_log)
self._sentry_mgr.enable()
@@ -143,7 +138,7 @@ class RedBase(BotBase, RpcMethodMixin):
:return:
"""
indict['owner_id'] = await self.db.owner()
indict["owner_id"] = await self.db.owner()
i18n.set_locale(await self.db.locale())
async def embed_requested(self, channel, user, command=None) -> bool:
@@ -214,14 +209,14 @@ class RedBase(BotBase, RpcMethodMixin):
curr_pkgs.remove(pkg_name)
async def load_extension(self, spec: ModuleSpec):
name = spec.name.split('.')[-1]
name = spec.name.split(".")[-1]
if name in self.extensions:
return
lib = spec.loader.load_module()
if not hasattr(lib, 'setup'):
if not hasattr(lib, "setup"):
del lib
raise discord.ClientException('extension does not have a setup function')
raise discord.ClientException("extension does not have a setup function")
if asyncio.iscoroutinefunction(lib.setup):
await lib.setup(self)
@@ -238,12 +233,24 @@ class RedBase(BotBase, RpcMethodMixin):
lib_name = lib.__name__ # Thank you
# find all references to the module
cog_names = []
# remove the cogs registered from the module
for cogname, cog in self.cogs.copy().items():
if cog.__module__.startswith(lib_name):
self.remove_cog(cogname)
cog_names.append(cogname)
# remove all rpc handlers
for cogname in cog_names:
if cogname.upper() in self.rpc_handlers:
methods = self.rpc_handlers[cogname]
for meth in methods:
self.unregister_rpc_handler(meth)
del self.rpc_handlers[cogname]
# first remove all the commands from the module
for cmd in self.all_commands.copy().values():
if cmd.module.startswith(lib_name):
@@ -262,7 +269,7 @@ class RedBase(BotBase, RpcMethodMixin):
del event_list[index]
try:
func = getattr(lib, 'teardown')
func = getattr(lib, "teardown")
except AttributeError:
pass
else:
@@ -279,19 +286,16 @@ class RedBase(BotBase, RpcMethodMixin):
if m.startswith(pkg_name):
del sys.modules[m]
if pkg_name.startswith('redbot.cogs'):
del sys.modules['redbot.cogs'].__dict__[name]
def register_rpc_methods(self):
rpc.add_method('bot', self.rpc__cogs)
rpc.add_method('bot', self.rpc__extensions)
if pkg_name.startswith("redbot.cogs"):
del sys.modules["redbot.cogs"].__dict__[name]
class Red(RedBase, discord.AutoShardedClient):
"""
You're welcome Caleb.
"""
async def shutdown(self, *, restart: bool=False):
async def shutdown(self, *, restart: bool = False):
"""Gracefully quit Red.
The program will exit with code :code:`0` by default.
@@ -314,4 +318,4 @@ class Red(RedBase, discord.AutoShardedClient):
class ExitCodes(Enum):
CRITICAL = 1
SHUTDOWN = 0
RESTART = 26
RESTART = 26

View File

@@ -1,27 +1,25 @@
import discord
from discord.ext import commands
from redbot.core import commands
async def check_overrides(ctx, *, level):
if await ctx.bot.is_owner(ctx.author):
return True
perm_cog = ctx.bot.get_cog('Permissions')
perm_cog = ctx.bot.get_cog("Permissions")
if not perm_cog or ctx.cog == perm_cog:
return None
# don't break if someone loaded a cog named
# permissions that doesn't implement this
func = getattr(perm_cog, 'check_overrides', None)
func = getattr(perm_cog, "check_overrides", None)
val = None if func is None else await func(ctx, level)
return val
def is_owner(**kwargs):
async def check(ctx):
override = await check_overrides(ctx, level='owner')
return (
override if override is not None
else await ctx.bot.is_owner(ctx.author, **kwargs)
)
override = await check_overrides(ctx, level="owner")
return override if override is not None else await ctx.bot.is_owner(ctx.author, **kwargs)
return commands.check(check)
@@ -32,9 +30,8 @@ async def check_permissions(ctx, perms):
return False
resolved = ctx.channel.permissions_for(ctx.author)
return all(
getattr(resolved, name, None) == value
for name, value in perms.items()
return resolved.administrator or all(
getattr(resolved, name, None) == value for name, value in perms.items()
)
@@ -76,11 +73,11 @@ async def is_admin_or_superior(ctx):
def mod_or_permissions(**perms):
async def predicate(ctx):
override = await check_overrides(ctx, level='mod')
override = await check_overrides(ctx, level="mod")
return (
override if override is not None
else await check_permissions(ctx, perms)
or await is_mod_or_superior(ctx)
override
if override is not None
else await check_permissions(ctx, perms) or await is_mod_or_superior(ctx)
)
return commands.check(predicate)
@@ -88,11 +85,11 @@ def mod_or_permissions(**perms):
def admin_or_permissions(**perms):
async def predicate(ctx):
override = await check_overrides(ctx, level='admin')
override = await check_overrides(ctx, level="admin")
return (
override if override is not None
else await check_permissions(ctx, perms)
or await is_admin_or_superior(ctx)
override
if override is not None
else await check_permissions(ctx, perms) or await is_admin_or_superior(ctx)
)
return commands.check(predicate)
@@ -101,6 +98,7 @@ def admin_or_permissions(**perms):
def bot_in_a_guild(**kwargs):
async def predicate(ctx):
return len(ctx.bot.guilds) > 0
return commands.check(predicate)
@@ -111,11 +109,8 @@ def guildowner_or_permissions(**perms):
return has_perms_or_is_owner
is_guild_owner = ctx.author == ctx.guild.owner
override = await check_overrides(ctx, level='guildowner')
return (
override if override is not None
else is_guild_owner or has_perms_or_is_owner
)
override = await check_overrides(ctx, level="guildowner")
return override if override is not None else is_guild_owner or has_perms_or_is_owner
return commands.check(predicate)

View File

@@ -26,16 +26,17 @@ def interactive_config(red, token_set, prefix_set):
if not prefix_set:
prefix = ""
print("\nPick a prefix. A prefix is what you type before a "
"command. Example:\n"
"!help\n^ The exclamation mark is the prefix in this case.\n"
"Can be multiple characters. You will be able to change it "
"later and add more of them.\nChoose your prefix:\n")
print(
"\nPick a prefix. A prefix is what you type before a "
"command. Example:\n"
"!help\n^ The exclamation mark is the prefix in this case.\n"
"Can be multiple characters. You will be able to change it "
"later and add more of them.\nChoose your prefix:\n"
)
while not prefix:
prefix = input("Prefix> ")
if len(prefix) > 10:
print("Your prefix seems overly long. Are you sure it "
"is correct? (y/n)")
print("Your prefix seems overly long. Are you sure that it's correct? (y/n)")
if not confirm("> "):
prefix = ""
if prefix:
@@ -48,12 +49,14 @@ def interactive_config(red, token_set, prefix_set):
def ask_sentry(red: Red):
loop = asyncio.get_event_loop()
print("\nThank you for installing Red V3 beta! The current version\n"
" is not suited for production use and is aimed at testing\n"
" the current and upcoming featureset, that's why we will\n"
" also collect the fatal error logs to help us fix any new\n"
" found issues in a timely manner. If you wish to opt in\n"
" the process please type \"yes\":\n")
print(
"\nThank you for installing Red V3 beta! The current version\n"
" is not suited for production use and is aimed at testing\n"
" the current and upcoming featureset, that's why we will\n"
" also collect the fatal error logs to help us fix any new\n"
" found issues in a timely manner. If you wish to opt in\n"
' the process please type "yes":\n'
)
if not confirm("> "):
loop.run_until_complete(red.db.enable_sentry.set(False))
else:
@@ -62,64 +65,92 @@ def ask_sentry(red: Red):
def parse_cli_flags(args):
parser = argparse.ArgumentParser(description="Red - Discord Bot",
usage="redbot <instance_name> [arguments]")
parser.add_argument("--version", "-V", action="store_true",
help="Show Red's current version")
parser.add_argument("--list-instances", action="store_true",
help="List all instance names setup "
"with 'redbot-setup'")
parser.add_argument("--owner", type=int,
help="ID of the owner. Only who hosts "
"Red should be owner, this has "
"serious security implications if misused.")
parser.add_argument("--co-owner", type=int, default=[], nargs="*",
help="ID of a co-owner. Only people who have access "
"to the system that is hosting Red should be "
"co-owners, as this gives them complete access "
"to the system's data. This has serious "
"security implications if misused. Can be "
"multiple.")
parser.add_argument("--prefix", "-p", action="append",
help="Global prefix. Can be multiple")
parser.add_argument("--no-prompt", action="store_true",
help="Disables console inputs. Features requiring "
"console interaction could be disabled as a "
"result")
parser.add_argument("--no-cogs",
action="store_true",
help="Starts Red with no cogs loaded, only core")
parser.add_argument("--load-cogs", type=str, nargs="*",
help="Force loading specified cogs from the installed packages. "
"Can be used with the --no-cogs flag to load these cogs exclusively.")
parser.add_argument("--self-bot",
action='store_true',
help="Specifies if Red should log in as selfbot")
parser.add_argument("--not-bot",
action='store_true',
help="Specifies if the token used belongs to a bot "
"account.")
parser.add_argument("--dry-run",
action="store_true",
help="Makes Red quit with code 0 just before the "
"login. This is useful for testing the boot "
"process.")
parser.add_argument("--debug",
action="store_true",
help="Sets the loggers level as debug")
parser.add_argument("--dev",
action="store_true",
help="Enables developer mode")
parser.add_argument("--mentionable",
action="store_true",
help="Allows mentioning the bot as an alternative "
"to using the bot prefix")
parser.add_argument("--rpc",
action="store_true",
help="Enables the built-in RPC server. Please read the docs"
"prior to enabling this!")
parser.add_argument("instance_name", nargs="?",
help="Name of the bot instance created during `redbot-setup`.")
parser = argparse.ArgumentParser(
description="Red - Discord Bot", usage="redbot <instance_name> [arguments]"
)
parser.add_argument("--version", "-V", action="store_true", help="Show Red's current version")
parser.add_argument(
"--list-instances",
action="store_true",
help="List all instance names setup with 'redbot-setup'",
)
parser.add_argument(
"--owner",
type=int,
help="ID of the owner. Only who hosts "
"Red should be owner, this has "
"serious security implications if misused.",
)
parser.add_argument(
"--co-owner",
type=int,
default=[],
nargs="*",
help="ID of a co-owner. Only people who have access "
"to the system that is hosting Red should be "
"co-owners, as this gives them complete access "
"to the system's data. This has serious "
"security implications if misused. Can be "
"multiple.",
)
parser.add_argument("--prefix", "-p", action="append", help="Global prefix. Can be multiple")
parser.add_argument(
"--no-prompt",
action="store_true",
help="Disables console inputs. Features requiring "
"console interaction could be disabled as a "
"result",
)
parser.add_argument(
"--no-cogs", action="store_true", help="Starts Red with no cogs loaded, only core"
)
parser.add_argument(
"--load-cogs",
type=str,
nargs="*",
help="Force loading specified cogs from the installed packages. "
"Can be used with the --no-cogs flag to load these cogs exclusively.",
)
parser.add_argument(
"--self-bot", action="store_true", help="Specifies if Red should log in as selfbot"
)
parser.add_argument(
"--not-bot",
action="store_true",
help="Specifies if the token used belongs to a bot account.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Makes Red quit with code 0 just before the "
"login. This is useful for testing the boot "
"process.",
)
parser.add_argument("--debug", action="store_true", help="Sets the loggers level as debug")
parser.add_argument("--dev", action="store_true", help="Enables developer mode")
parser.add_argument(
"--mentionable",
action="store_true",
help="Allows mentioning the bot as an alternative to using the bot prefix",
)
parser.add_argument(
"--rpc",
action="store_true",
help="Enables the built-in RPC server. Please read the docs prior to enabling this!",
)
parser.add_argument("--token", type=str, help="Run Red with the given token.")
parser.add_argument(
"--no-instance",
action="store_true",
help=(
"Run Red without any existing instance. "
"The data will be saved under a temporary folder "
"and deleted on next system restart."
),
)
parser.add_argument(
"instance_name", nargs="?", help="Name of the bot instance created during `redbot-setup`."
)
args = parser.parse_args(args)
@@ -129,4 +160,3 @@ def parse_cli_flags(args):
args.prefix = []
return args

View File

@@ -34,14 +34,12 @@ class CogManager:
install new cogs to, the default being the :code:`cogs/` folder in the root
bot directory.
"""
def __init__(self, paths: Tuple[str]=()):
def __init__(self, paths: Tuple[str] = ()):
self.conf = Config.get_conf(self, 2938473984732, True)
tmp_cog_install_path = cog_data_path(self) / "cogs"
tmp_cog_install_path.mkdir(parents=True, exist_ok=True)
self.conf.register_global(
paths=(),
install_path=str(tmp_cog_install_path)
)
self.conf.register_global(paths=(), install_path=str(tmp_cog_install_path))
self._paths = [Path(p) for p in paths]
@@ -158,7 +156,7 @@ class CogManager:
if path == await self.install_path():
raise ValueError("Cannot add the install path as an additional path.")
all_paths = _deduplicate(await self.paths() + (path, ))
all_paths = _deduplicate(await self.paths() + (path,))
# noinspection PyTypeChecker
await self.set_paths(all_paths)
@@ -225,8 +223,10 @@ class CogManager:
if spec:
return spec
raise RuntimeError("No 3rd party module by the name of '{}' was found"
" in any available path.".format(name))
raise RuntimeError(
"No 3rd party module by the name of '{}' was found"
" in any available path.".format(name)
)
async def _find_core_cog(self, name: str) -> ModuleSpec:
"""
@@ -247,10 +247,11 @@ class CogManager:
"""
real_name = ".{}".format(name)
try:
mod = import_module(real_name, package='redbot.cogs')
mod = import_module(real_name, package="redbot.cogs")
except ImportError as e:
raise RuntimeError("No core cog by the name of '{}' could"
"be found.".format(name)) from e
raise RuntimeError(
"No core cog by the name of '{}' could be found.".format(name)
) from e
return mod.__spec__
# noinspection PyUnreachableCode
@@ -284,7 +285,7 @@ class CogManager:
async def available_modules(self) -> List[str]:
"""Finds the names of all available modules to load.
"""
paths = (await self.install_path(), ) + await self.paths()
paths = (await self.install_path(),) + await self.paths()
paths = [str(p) for p in paths]
ret = []
@@ -341,8 +342,7 @@ class CogManagerUI:
Add a path to the list of available cog paths.
"""
if not path.is_dir():
await ctx.send(_("That path does not exist or does not"
" point to a valid directory."))
await ctx.send(_("That path does not exist or does not point to a valid directory."))
return
try:
@@ -398,7 +398,7 @@ class CogManagerUI:
@commands.command()
@checks.is_owner()
async def installpath(self, ctx: commands.Context, path: Path=None):
async def installpath(self, ctx: commands.Context, path: Path = None):
"""
Returns the current install path or sets it if one is provided.
The provided path must be absolute or relative to the bot's
@@ -416,8 +416,9 @@ class CogManagerUI:
return
install_path = await ctx.bot.cog_mgr.install_path()
await ctx.send(_("The bot will install new cogs to the `{}`"
" directory.").format(install_path))
await ctx.send(
_("The bot will install new cogs to the `{}` directory.").format(install_path)
)
@commands.command()
@checks.is_owner()
@@ -435,22 +436,20 @@ class CogManagerUI:
unloaded = sorted(list(unloaded), key=str.lower)
if await ctx.embed_requested():
loaded = ('**{} loaded:**\n').format(len(loaded)) + ", ".join(loaded)
unloaded = ('**{} unloaded:**\n').format(len(unloaded)) + ", ".join(unloaded)
loaded = ("**{} loaded:**\n").format(len(loaded)) + ", ".join(loaded)
unloaded = ("**{} unloaded:**\n").format(len(unloaded)) + ", ".join(unloaded)
for page in pagify(loaded, delims=[', ', '\n'], page_length=1800):
e = discord.Embed(description=page,
colour=discord.Colour.dark_green())
for page in pagify(loaded, delims=[", ", "\n"], page_length=1800):
e = discord.Embed(description=page, colour=discord.Colour.dark_green())
await ctx.send(embed=e)
for page in pagify(unloaded, delims=[', ', '\n'], page_length=1800):
e = discord.Embed(description=page,
colour=discord.Colour.dark_red())
for page in pagify(unloaded, delims=[", ", "\n"], page_length=1800):
e = discord.Embed(description=page, colour=discord.Colour.dark_red())
await ctx.send(embed=e)
else:
loaded_count = '**{} loaded:**\n'.format(len(loaded))
loaded_count = "**{} loaded:**\n".format(len(loaded))
loaded = ", ".join(loaded)
unloaded_count = '**{} unloaded:**\n'.format(len(unloaded))
unloaded_count = "**{} unloaded:**\n".format(len(unloaded))
unloaded = ", ".join(unloaded)
loaded_count_sent = False
unloaded_count_sent = False

View File

@@ -2,3 +2,4 @@
from discord.ext.commands import *
from .commands import *
from .context import *
from .errors import *

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