Compare commits

..

549 Commits

Author SHA1 Message Date
Toby Harradine
dc87f6c251 Bump version to 3.0.2
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-25 08:43:42 +11:00
Toby Harradine
431cdf1ad4 [Permissions] Fix typo in cog_add event 2019-02-25 08:43:02 +11:00
ZeLarpMaster
c2bfcfdc64 [Trivia] Fix typo in cars.yaml (#2475) 2019-02-25 08:42:51 +11:00
Caleb Johnson
343132a371 [Audio] Connect to lavalink in the background (#2460)
Also:
- restart and reconnect if connection settings change
  - shutdown and restart if not configured to use external
- show a message in [p]play et al. when the connection hasn't been made
- move the JAR download to manager so audio.py can access it
- only start if no process exists
- bump red-lavalink to 0.2.3

Resolves #2306
2019-02-17 09:47:19 +11:00
Toby Harradine
e97240e568 Bump version to 3.0.1
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-16 12:25:26 +11:00
Michael H
6383e9f738 [Utils] Add filters for spoiler markdown (#2401)
This also wraps some fields of the modlog with the same sanitization, as well as the `[p]names` command.
2019-02-16 11:56:20 +11:00
Michael H
d96062226d [Audio] Remove players which no longer have a guild. (#2414)
Cleanup players when the bot has one for a guild it leaves.

Bumps Red-Lavalink to v0.2.2
2019-02-16 11:17:18 +11:00
Twentysix
eebed27fe7 [Downloader] [p]pipinstall: Handle no args 2019-02-16 11:06:56 +11:00
Toby Harradine
278abecdef Add missing version bumps from 7c8ac9c
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-16 11:06:27 +11:00
ZeLarpMaster
21ac81679f [Trivia] Fix typo in cars.yaml (#2456) 2019-02-16 09:17:43 +11:00
DiscordLiz
5cf0c98bca Prevent error when ctx.send_interactive prompt is deleted (#2447)
Supress excpetions which may occur when attempting to delete a prompt.

fixes #2380
2019-02-16 09:17:34 +11:00
Toby Harradine
7c8ac9cd54 Update dependencies and copyright year (#2436)
- aiohttp 3.5
- websockets 7
- Rapptz/discord.py@700dbb5
- A few others

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-16 09:17:02 +11:00
Toby Harradine
cfd8ef6025 Guard parsing of CLI args in launcher, setup scripts (#2432)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-16 09:06:28 +11:00
PredaaA
174754f800 Update help_formatter.py (#2431) 2019-02-16 09:06:11 +11:00
Twentysix
53aa84bb94 [p]userinfo: Handle target w/ 'None' Member.joined_at (#2426) 2019-02-16 09:06:04 +11:00
Toby Harradine
a2c0bddd6b Default rules for subcommands precede supercommands (#2422)
This incorporates default rules into the same resolution techniques used by concrete rules.

Resolves #2313.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-16 09:05:58 +11:00
zephyrkul
7dd3310377 [Downloader] Use shlex for subprocesses (#2421) 2019-02-16 09:05:50 +11:00
Toby Harradine
0fa2e36629 Always tick Voice requirements on startup screen (#2413)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-16 09:05:41 +11:00
zephyrkul
3b38a5f9b9 Send help on empty [p]load/unload/reload (#2410)
Rather than attempting to load / reload / unload nothing, send command help on bare `[p]load`, etc. commands.
2019-02-16 09:05:32 +11:00
zephyrkul
f61e8e0907 [Core] Utilize consume rest, Union (#2407) 2019-02-16 09:05:17 +11:00
Michael H
5fc8f9fee1 prevent traceback (#2406)
* prevent traceback related to  Rapptz/discord.py#1638

* formatting
2019-02-16 09:05:08 +11:00
Michael H
644bb0a560 Improve usability of warnings/unwarn\n resolves #2403 (#2404) 2019-02-16 09:04:58 +11:00
PredaaA
93ea773b7c [Core cmds] [p]servers: handle message deletion (#2400) 2019-02-16 09:04:50 +11:00
Redjumpman
655b5a96ba [Utils] Fix for MessagePredicate.lower_contained_in (#2399)
Added a missing str.lower() method when checking to see if the content is in the list.
2019-02-16 09:04:41 +11:00
Toby Harradine
bbe88293ab Use python-Levenshtein-wheels (#2393)
This removes the compiler detection logic in setup.py. python-Levenshtein-wheels includes pre-built wheels for virtually all operating systems and architectures we support.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-02-16 09:04:32 +11:00
Toby Harradine
91258fea78 Bump version to 3.0.0
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-28 14:52:14 +11:00
Toby Harradine
5c1c6e1f03 Remove version from help message
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-28 14:49:55 +11:00
Toby Harradine
6d5762d711 Move Red-Lavalink to main requirements
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-28 14:38:43 +11:00
Toby Harradine
05bef917ae Vendor discord.py (#2387)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-28 14:14:36 +11:00
Caleb Johnson
348277bcbd [Audio] Lavalink 3.0/3.1 compatibility updates (#2272)
- Update to red-lavalink v0.2.0 (blocked by Cog-Creators/Red-Lavalink#41)
- Force lavalink to use TLSv1.2 on java 11+ (blocked by #2270)

I would add equalizer support, but there's no way to know the full
Lavalink version and thus whether it's supported ahead of time.
2019-01-28 12:43:21 +11:00
Iangit1
158c4f741b Grammar in ask_sentry and interactive_config (#2383) 2019-01-21 09:07:55 +11:00
Michael H
1c4193cce2 [Permissions] Quick extra comment of importance (#2379) 2019-01-18 14:48:00 +11:00
Michael H
849ade6e58 Reconcile permission hooks with ctx.permission_state (#2375)
Resolves #2374.

See mod.py's voice mute for an example of why this may be necessary.
2019-01-18 14:45:34 +11:00
Toby Harradine
0d4e6a0865 Fix MongoDB to JSON migration and warn about Mongo driver (#2373)
Resolves #2372.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-18 11:26:33 +11:00
Toby Harradine
32bd47e105 Bump version to 3.0.0rc3.post1
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-11 13:09:40 +11:00
Toby Harradine
1bb5d698cc Make Travis only do py36 tox when deploying
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-11 13:04:00 +11:00
Toby Harradine
9752a9c719 Bump version to 3.0.0rc3 (#2367)
Also updated some dependencies, including discord.py.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-11 11:10:01 +11:00
Toby Harradine
7973babe4b Catch exceptions in [p]backup (#2363)
Resolves #2354.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-11 09:07:37 +11:00
Michael H
78e4b578e2 [Utils] Tunnel minor fixes (#2366)
- Tunnel uses a safe max size (Max size is related to maximum payload, not maximum file size)
  - Checks attachment sizes prior to download
2019-01-10 19:46:49 +11:00
Toby Harradine
8eb8848898 [Mod] Context-based voice checks (#2351)
- Removed `redbot.cogs.mod.checks` module
- Moved logic for formatting a user-friendly list of permissions to `redbot.core.utils.chat_formatting`
- `[p]voice(un)ban` and `[p](un)mute voice` now check permissions in the user's voice channel

Resolves #2296.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-10 11:35:37 +11:00
Toby Harradine
aac1460240 [Utils] Exit menu silently when message is deleted (#2344)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-10 11:33:38 +11:00
Toby Harradine
dde5582669 [CogManager] Removal of implicit paths and general cleanup (#2345)
- Removed memory-sided `CogManager._paths` attribute, as it has no practical use.
- `[p]removepath` now removes the actual path displayed with the index specified in `[p]paths`.
- New method for retreiving a deduplicated list of user-defined paths as `Path` objects
- General cleanup so we don't have to do so much head-scratching next time an issue arises here

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2019-01-06 11:02:58 +11:00
Toby Harradine
aa854cf1f9 [CustomCom] Insert space before newline separating CC previews (#2350)
Resolves #2295.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-12-29 16:54:32 +01:00
Toby Harradine
2bd05a5a04 Fix JSON to Mongo migration (#2346)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-12-29 10:51:31 +11:00
Toby Harradine
3a8da1f82b [Cleanup] Fix cleanup after
Resolves #2343.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-12-22 09:19:25 +11:00
Toby Harradine
811634a2b0 [Permissions] Help menu respects rules (#2339)
Resolves #2249.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-12-21 13:30:13 +11:00
Toby Harradine
2512320b30 [DataManager] Don't copy bundled data to cog data folder (#2342)
- `load_bundled_data` is now deprecated and obselete
- Bundled data is now no longer copied to the cog's data folder, greatly simplifying its operations under the hood. Instead, `bundled_data_path` will now return the path to the folder from which the files would have previously been copied.

Resolves #2329.

Resolves #2280.
2018-12-21 13:26:00 +11:00
Michael H
db03faf042 Make webhooks automod immune (#2337)
Resolves #2336.
2018-12-21 11:43:46 +11:00
Toby Harradine
701259158f [Permissions] Always respect default rules (#2341)
Resolves #2340.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-12-21 10:35:17 +11:00
Kowlin
a5efdc6492 [Mod] Handle invalid nicknames in [p]rename (#2311) 2018-12-16 11:21:36 +11:00
Kowlin
38b15ded87 [Mod] Fixed loud RuntimeError on modlog cases (#2331) 2018-12-16 10:26:48 +11:00
Michael H
351749dff6 [Permissions] Find things uniquely for models (#2258)
This is a safety measure to prevent accidentally passing a model which has the same name as another model, potentially modifying rules for the unwanted one.
2018-12-16 10:09:18 +11:00
Michael H
2d9912cea7 prevent locking out owner (#2317) 2018-12-13 18:38:03 +01:00
NNTin
c4ab34a049 V3: regex extension on java -version (#2316)
* regex extension on java -version

* make it a non capturing group

* alphanumeric matching

* Match specification: Style, line length

* Update manager.py
2018-12-13 18:28:00 +01:00
Michael H
985e7b3c6d swap unsafe yaml.load usage for yaml.safe_load (#2324)
Related to #2323

Recommend additionally adding a step in CI
ensuring use of `yaml.load` is prevented from existing in the code base.
2018-12-13 18:19:27 +01:00
palmtree5
7546c50226 [Streams] Toggle mentions on for roles being alerted (#2252)
Resolves #1831.
2018-11-24 10:46:38 +11:00
aikaterna
6435f6b882 [Audio] Match openJDK 11 on Ubuntu (#2270)
Using the OpenJDK 11 from java.net on Ubuntu 18 reports "11" as the version, which failed the Java version check on loading audio on a new instance. This change will return "11 0" as the version, passing the check, instead of just "11".
2018-11-24 10:45:37 +11:00
aikaterna
bbccb671b8 [Audio] Disallow seek during active vote (#2290)
Users were able to use seek to skip songs while voteskip was active. This change prevents users from doing so.
2018-11-24 10:43:23 +11:00
Twentysix
8abb24bc01 [Core] [p]leave: Fix incorrect response handling (#2302) 2018-11-24 10:42:05 +11:00
aikaterna
419008f644 [Downloader] Findcog fix with no repo installed (#2304)
The findcog command errored out if a repo was removed. This change informs the user that the repo is not installed instead of erroring out.
2018-11-24 10:39:54 +11:00
odinair
d17c2430d7 [Streams] Fix alerts with no configured mentions (#2305) 2018-11-24 10:14:43 +11:00
Michael H
ca533f8937 [JsonIO] race condition fix (#2308)
* race condition fix

* style fix
2018-11-24 10:11:59 +11:00
Michael H
9d22d5b7b5 [Cleanup] Correct handling of command message (#2310)
Resolves #2307
2018-11-24 10:04:08 +11:00
Kowlin
2846dce6ea [Mod] [p]modset: Fix KeyError (#2279)
Co-authored-by: aikaterna <20862007+aikaterna@users.noreply.github.com>
2018-11-14 23:35:56 +01:00
aikaterna
9973b2e3b8 [General] [p]urban: Handle no embeds (#2285) 2018-11-14 23:21:11 +01:00
aikaterna
d008a2559a [General] Clearer error message for [p]rps (#2284) 2018-11-14 22:56:08 +01:00
aikaterna
7f3a0b8a88 [Streams] Add help for streamset (#2287) 2018-11-06 10:26:55 +11:00
aikaterna
d0fca373ba [Audio] Local track verify on playlist start (#2271) 2018-11-06 09:44:26 +11:00
FixedThink
451c4c9d54 [Trivia] Fix text formatting issue (#2269)
The "Question number {}" message was being emboldened twice from the left side, making the bot send messages with two redundant asterisks.
2018-11-06 08:58:10 +11:00
zephyrkul
a59002275d [Economy] Fix TypeError in [p]payouts (#2263) 2018-11-06 08:55:29 +11:00
Toby Harradine
99bbde7be9 Reject bad perm kwargs in check decorators (#2289)
Also fixed a misspelled kwarg in reports.

Also now raising TypeError for an empty `@checks.has_permissions()` decorator.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-11-05 15:07:56 +11:00
Kowlin
92dbd14006 Fixes typos in permissions (#2288)
causing the checks to be thrown out of the window.
2018-11-05 04:19:54 +01:00
palmtree5
6e9243f6e9 Drop unneeded .format in [p]triviaset override (#2268) 2018-10-23 17:40:51 -08:00
Toby Harradine
8bba860f85 Bump version to 3.0.0rc2
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-16 09:37:23 +11:00
Toby Harradine
d2d26835c3 [Economy] Detect max balance and prevent OverflowError (#2211)
Resolves #2091.

This doesn't fix every OverflowError with MongoDB; but at least the seemingly easiest one to achieve with core cogs.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-16 09:30:53 +11:00
Toby Harradine
aff62a8006 [Downloader] Unload extensions on uninstall (#2243)
Resolves #2216.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-16 09:19:32 +11:00
Toby Harradine
b5fd28ef7c [CustomCom] Better display for [p]cc list (#2215)
Uses a menu, optionally embedded with respect to the embed settings, for scrolling through the custom command list, each cc with a ~50 character preview. Format is purposefully similar to the help menu.

Resolves #2104.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-16 08:39:44 +11:00
Michael H
c510ebe5e5 [Downloader] Only prompt reload of loaded cogs (#2233) 2018-10-15 23:29:56 +11:00
Toby Harradine
5ba95090d9 [Streams] Suppress HTTPExceptions on load (#2228)
Resolves #2227.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-15 22:31:14 +11:00
Toby Harradine
ad51fa830b [Cleanup] [p]cleanup bot includes aliases and CCs (#2213)
Resolves #1920.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-15 22:29:07 +11:00
Christopher Rice
1ba922eba2 [Downloader] Add missing prefix format kwarg (#2238)
Fixes #2237.
2018-10-14 17:11:16 +11:00
Christopher Rice
9588a5740c [Downloader] Define Translator in converters module (#2239)
Fixes #2236
2018-10-14 17:09:54 +11:00
Toby Harradine
7cd765d548 Fix permissions hook removal (#2234)
Some in-progress work slipped through #2149, and I figure it should be fixed before RC2.

I've also just decided to allow discovery of permissions hooks from superclasses as well. We should try to be more aware of the possibility of cog superclasses moving forward.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-14 11:52:39 +11:00
zephyrkul
6022c0f7d7 [Mod] Mute/unmute bugfixes (#2230)
- Helper methods mute_user and unmute_user now take GuildChannel instead of solely TextChannel.
- The bot will not attempt to mute a member with the Administrator permission, as that permission bypasses channel overwrites anyway. The bot will still unmute members with the Administrator permission (see #2076).
- Audit reasons are now specified for mass mutes / unmutes.
- A few typos and missing keyword specifiers were corrected.
- Streamlined some logic and used some already-existing functions.
2018-10-12 15:47:39 +11:00
palmtree5
0548744e94 [V3 Cleanup] fix error in cleanup user (#2225) 2018-10-11 14:36:45 +02:00
Toby Harradine
8b2d115335 [Audio] Rename current_build to current_version in Config (#2219)
Renames the `current_build` key to `current_version`. This means the `current_version` key will always be a dict and never a list, and `current_build` having no defaults means it won't mess with `[p]audioset settings`.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-11 12:02:02 +11:00
Toby Harradine
094735566d [Warnings] Fix actions not being taken (#2218)
When multiple warning actions were registered, and the user didn't exceed the points for the highest action on the list, no action was taken.

Resolves #2106.

Also commented out the casetype registration for warnings, since it's not actually using modlog yet.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-11 11:54:11 +11:00
Toby Harradine
f7b1f9f0dc [MongoDB] Escape special characters in keys (#2212)
Essentially resolves #2038, although this is escaping and not rejecting keys as that issue implies.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-11 11:20:42 +11:00
Toby Harradine
ce25011f0d [Config] Cast keys to str on get/set/clear (#2217)
This is a step towards a more consistent front-end behaviour of Config, where errors are either circumvented or raised in the same way regardless of the driver being used.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-11 11:18:57 +11:00
bobloy
f85034eb27 [Trivia] Add On/Off as alternatives for YAML bools (#2177) 2018-10-09 22:05:37 +11:00
Toby Harradine
849755ecd2 [Core] Fix errors with [p]backup on MongoDB (#2210)
Resolves #2094.

This command needs some more fixing and cleaning up than this, this is just a simple bugfix which gets it mostly working for now.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-09 15:30:27 +11:00
Toby Harradine
9217275908 [Permissions] Fix ValueError for "default" rule in config/ACL (#2200)
This was thrown when the "default" key existed and Permissions tried to iterate over the list mapping keys as ints.

Also fixed some issues with saving config with keys as `int` instead of `str`.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-09 15:14:59 +11:00
zephyrkul
9e13ca45e6 [Mod] Fix KeyError in modset (#2208) 2018-10-09 09:04:51 +11:00
aikaterna
46c38a28eb [Alias] Fix alias help (#2194)
Alias help would only return the first character of the invoked command previously. This change shows help for basic commands that are aliased (i.e. just `ping`) or aliased commands that have an argument included (i.e. `audioset role beep` with `beep` being a role name)
2018-10-08 18:23:32 +11:00
El Laggron
76bbcf2f8c [Core] Support already loaded packages in [p]load (#2116) 2018-10-08 08:18:28 +11:00
ASSASSIN0831
ee7e8aa782 [Economy] Revert change to payday message (#2203)
in the updates were for the i18n translation strings the payday command message was accidentally changed to +(amount) (new balance). This changes it back to its original message +(amount) (currency name)
2018-10-08 07:39:30 +11:00
Toby Harradine
fd0abc250d [Audio] Fix type mismatch between config defaults and set value (#2201)
current_build is now set as a dict, but its default was a list.

This resolves the error an `[p]audioset settings`.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-07 19:33:04 +11:00
Toby Harradine
847f9fc8d1 [CustomCommands] Find default converters properly (#2199)
The new `redbot.core.commands.converter` module was causing default converters to never be found.

Also cleaned up some of the other code (made some methods static, fixed some typing violations)

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-07 12:58:08 +11:00
zephyrkul
046e98565e [Cleanup] use message_filter() over check() param (#2180)
Cleanup's helper method to collect messages to delete was incorrectly filtering by check rather than message_filter, causing delete_after to be ignored.
2018-10-07 10:19:56 +11:00
Toby Harradine
71eddc89ea [Mod] Fix unresolved reference to Member.permissions in reinvite logic
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-07 09:42:25 +11:00
aikaterna
9730a424ec [Admin] Selfrole list formatting (#2193)
Selfrole list needed a return in between the header and the list.
2018-10-07 08:46:32 +11:00
aikaterna
7b260cdafc [Audio] Playlist list, local queue, DJ Role fix (#2191)
Fix for `playlist list`: added forgotten variable plus return for formatting (fixes #2190)

i18n addditions: DJ Role toggle, Thumbnail display toggle

`audioset settings`: added missed end of line return

`queue` fix: added indentation to not have the currently playing song info repeated in the queue display when playing local songs

`local play` and `local folder` now display the appropriate menu when DJ role is on.
2018-10-07 08:42:13 +11:00
aikaterna
4369095a51 [Economy] Fix for bank set (#2192)
i18n variable was wrong.
2018-10-07 08:26:33 +11:00
Toby Harradine
1c706e8c45 Bump version to 3.0.0rc1.post1
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-06 19:25:14 +10:00
Toby Harradine
91029b73e5 Use our own redbot.core.VersionInfo over distutils.StrictVersion (#2188)
* Implements our required subset of PEP 440 in redbot.core.VersionInfo
* Added unit tests for version string parsing and comparisons

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-06 19:11:05 +10:00
zephyrkul
de4b42a11e [mongo] fix missing URI variable assignment (#2186) 2018-10-06 14:48:03 +10:00
Toby Harradine
03230b6386 Remove branch condition from Travis deployment
So we can automatically deploy from release branches
2018-10-06 14:07:27 +10:00
Toby Harradine
4dbf2796c0 Bump version to 3.0.0rc1
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-06 10:26:59 +10:00
Twentysix
03892f5ef1 [Utils] Markdown helpers escape special chars (#2182)
Escaping italics (`*`) that get passed to `italics()` doesn't work, the string still won't show up properly.
2018-10-06 10:14:11 +10:00
bobloy
fdf3f86ab0 [Utils] Allow menu() to be used DM (#2183)
`ctx.me` handles using `ctx.guild.me` if `ctx.guild is not None`
`ctx.guild.me` directly errors in DMs.
2018-10-06 10:02:09 +10:00
Toby Harradine
7b15ad5989 Merge branch V3/feature/i18n_pass into V3/develop (#2024)
[i18n] Improves the coverage and quality of extracted user-facing string literals in the `redbot.core` package and makes them less ambiguous for the translator.
2018-10-06 09:58:45 +10:00
Toby Harradine
443f2ca556 [i18n] Pass over modlog, permissions, reports, streams, trivia, warnings
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-06 09:01:04 +10:00
Toby Harradine
fa692ccc0b [i18n] Pass over economy, filter, general, image, mod
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-06 09:00:31 +10:00
Toby Harradine
0c3d8af8f4 [i18n] Pass over bank, cleanup, customcom, dataconverter, downloader
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-06 08:57:49 +10:00
Toby Harradine
3a20c11331 [i18n] User-facing string pass over admin, alias and audio 2018-10-06 08:43:19 +10:00
Michael H
aa8c9c350e [i18n] Start work on named format arguments (#1795) 2018-10-06 08:42:38 +10:00
Michael H
139329233a [Utils/Trivia] Handle smart quotes (#2162)
Adds a new filter function for substituting out smart-quotes.

Makes trivia use it.
2018-10-06 08:39:52 +10:00
Michael H
d79996aeea [Downloader] Better user facing feedback on cog update (#2165) 2018-10-06 08:24:55 +10:00
Toby Harradine
fb839084fe Merge branch V3/feature/predicates into V3/develop (#1986)
Predicate utility to replace common boilerplate predicates for event waiting.
2018-10-06 08:15:25 +10:00
Toby Harradine
dea9dde637 [Utils] Finish and Refactor Predicate Utility (#2169)
* Uses classmethods to create predicates
* Classmethods allow using a combination of different parameters to describe context
* Some predicates assign a captured `result` to the predicate object on success
* Added `ReactionPredicate` equivalent to `MessagePredicate`
* Added `utils.menus.start_adding_reactions`, a non-blocking method for adding reactions asynchronously
* Added documentation
* Uses these new utils throughout the core bot
Happened to also find some bugs in places, and places where we were waiting for events without catching `asyncio.TimeoutError`

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-06 08:07:09 +10:00
zephyrkul
ebc657dcc6 [Config] Add Group.clear_raw method (#2178)
Adds a `clear_raw` method to Group objects, similar to the existing `get_raw` and `set_raw` methods.
Documentation included.
2018-10-05 13:03:27 +10:00
zephyrkul
80506856fb [Checks] Preserve backwards compatibility in deprecated functions (#2176)
* [checks] preserve backwards compatability

* [checks] use correct ctx.author
2018-10-04 10:26:44 +10:00
palmtree5
93a0e45c34 [Filter] implement exempting channels from the filter (#2064) 2018-10-03 08:43:04 +10:00
bobloy
3cb2b95121 [V3 Cleanup] Cleanup Before command (#2171)
Adds the ability to cleanup before a specified message id. Requires passing a number of messages to delete to keep with syntax of cleanup self/user
2018-10-03 08:28:10 +10:00
bobloy
a04869ab27 [Trivia] Fix misuse of list.append (#2172) 2018-10-03 08:22:42 +10:00
Twentysix
c69b1185d3 [Black] Don't normalize underscores in numeric literals (#2174) 2018-10-03 08:19:30 +10:00
Toby Harradine
ad7466a026 Dependency Update (#2175)
##### Core requirements
* _discord.py_ Rapptz/discord.py@77239e4 -> Rapptz/discord.py@836ae73
* _aiohttp-json-rpc_ 0.11.1 -> 0.11.2
* _aiohttp_ 3.3.2 -> 3.4.4

##### [test]
* _pytest_ 3.8.1 -> 3.8.2

##### [docs]
* _packaging_ 17.1 -> 18.0
* _pyparsing_ 2.2.1 -> 2.2.2
* _six_ Removed duplicate entry

##### [style]
* _black_ 18.6b4 -> 18.9b0
* _click_ 6.7 -> 7.0

### Notes
- `extra_requires` in setup.py is now a module-level global
- Some style changes have occurred after the _black_ update

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-03 08:10:13 +10:00
zephyrkul
54dad2a604 [Mod] Use list.copy() for tempban expirations (#2166)
Fixes an error left behind from #2161 (modifying a list while iterating over it)
2018-10-02 18:46:11 +10:00
bobloy
d5899fae83 [RPC] Add missing await in rpc_load (#2167) 2018-10-02 18:44:38 +10:00
Redjumpman
5d44bfabed [Utils] Initial Work on Predicate Utility (#1985)
* Add files via upload

* Update predicates.py

Changed sender from a discord.Member object to ctx.
Added a channel check.
Combined the same method and channel method into a validator and applied through-out.
valid_role and has_role methods now check for either an id or a name.
contained now uses string.lower() when testing for membership in a collection.

Signed-off-by: Redjumpman <redjumpman@users.noreply.github.com>
2018-10-02 16:19:40 +10:00
Ryan
b6c8be5f43 [MongoDB] Support mongodb+srv protocol (#2159) 2018-10-01 16:49:29 +10:00
aikaterna
b2abfc5710 [Audio] Playlist list and notify msg changes (#2155) 2018-10-01 16:44:46 +10:00
zephyrkul
a9b328ff3c [Mod] Remove tempban from data after unbanning (#2161)
Resolves #2160.

Also resolves another issue where the bot will error out when getting the guild's invites when it doesn't have the Manage Server permission.
2018-10-01 14:15:51 +10:00
Toby Harradine
0870403299 Permissions redesign (#2149)
API changes:
- Cogs must now inherit from `commands.Cog` (see #2151 for discussion and more details)
- All functions which are not decorators in the `redbot.core.checks` module are now deprecated in favour of their counterparts in `redbot.core.utils.mod`. This is to make this module more consistent and end the confusing naming convention.
- `redbot.core.checks.check_overrides` function is now gone, overrideable checks can now be created with the `@commands.permissions_check` decorator
- Command, Group, Cog and Context have some new attributes and methods, but they are for internal use so shouldn't concern cog creators (unless they're making a permissions cog!).
- `__permissions_check_before` and `__permissions_check_after` have been replaced:  A cog method named `__permissions_hook` will be evaluated as permissions hooks in the same way `__permissions_check_before` previously was. Permissions hooks can also be added/removed/verified through the new `*_permissions_hook()` methods on the bot object, and they will be verified even when permissions is unloaded.
- New utility method `redbot.core.utils.chat_formatting.humanize_list`
- New dependency [`schema`](https://github.com/keleshev/schema)

User-facing changes:
- When a `@bot_has_permissions` check fails, the bot will respond saying what permissions were actually missing.
- All YAML-related `[p]permissions` subcommands now reside under the `[p]permissions acl` sub-group (tbh I still think the whole cog has too many top-level commands)
- The YAML schema for these commands has been changed
- A rule cannot be set as allow and deny at the same time (previously this would just default to allow)

Documentation:
- New documentation for `redbot.core.commands.requires` and `redbot.core.checks` modules
- Renewed documentation for the permissions cog
- `sphinx.ext.doctest` is now enabled

Note: standard discord.py checks will still behave exactly the same way, in fact they are checked before `Requires` is looked at, so they are not overrideable. 

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-10-01 13:19:25 +10:00
Toby Harradine
f07b78bd0d Fix help command with cogs (#2156)
This bug was introduced in #2122 (whoops)

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-09-30 14:19:25 +10:00
Toby Harradine
b2497386bb Update dependencies (#2148)
Core dependencies:
- discord.py: Rapptz/discord.py@00a659c6 -> Rapptz/discord.py@00a659c6
- multidict: 4.4.0 -> 4.4.2

[docs]
- pyparsing: 2.2.0 -> 2.2.1
- sphinx: 1.7.8 -> 1.7.9

[test]
- pytest: 3.7.4 -> 3.8.1

[style]
- toml: 0.9.4 -> 0.9.6

----

I've also replaced usages of `discord.utils.get(guild.roles, id=id)` with the new O(1) `guild.get_role(id)` method.

Signed-off-by: Toby <tobyharradine@gmail.com>
2018-09-25 16:09:36 +10:00
Michael H
f8558b98c1 Automated mod action immunity settings (#2129)
Refactors, and fixes some logic in filter which was encountered while
applying the settings to core
2018-09-25 11:30:28 +10:00
Michael H
84ac5f3952 JsonIO atomic write improvements (#2132)
Use `async with Lock` instead of deprecated `with await lock` usage.

Forces a file fsync prior and a directory fsync (where available) after rename to prevent issues with left behind temp files.

Also should clarify: this is not threadsafe. Comments were clarified, function names remain misleading.
2018-09-24 18:29:14 +10:00
Michael H
404800c556 Hackban fixes (#2128)
If the member is in the guild, delegates to existing ban logic.

Additionally, check that we have ban perms prior to attempting to fetch the existing ban list.

Fixes #2127.
2018-09-24 18:21:21 +10:00
Toby Harradine
415385b747 [Downloader] Fix git pull command (#2146)
I somehow managed to botch this in #2121

Signed-off-by: Toby Harradine <t.harradine@student.unsw.edu.au>
2018-09-24 13:20:30 +10:00
Toby Harradine
f7dbaca340 Refactor fuzzy help and clean up help command (#2122)
What's changed:
- Fixed issues mentioned on #2031
- Fuzzy help displays more like help manual
- Fuzzy help is easier and more flexible to use
- Fuzzy help string-matching ratio lowered to 80
- Help formatter is more extendable
- Help command has been optimized, cleaned up and better incorporates fuzzy help
- Added async_filter and async_enumerate utility functions because I was using them for this PR, then no longer needed them, but decided they might be useful anyway.
- Added `Context.me` property which is a shortcut to `Context.guild.me` or `Context.bot.user`, depending on the channel type.
2018-09-24 10:34:39 +10:00
bobloy
32b4c6ce86 [Image] Remove V2 relics from command decorators (#2138) 2018-09-23 23:44:01 +10:00
zephyrkul
a3c36d4bde NoneType check for module in should_log_sentry (#2139)
Allows for a lack of module (which returns False) for `should_log_sentry`. This allows for, say, commands to be added by the Dev cog. ( ͡ಠ ʖ̯ ͡ಠ)
2018-09-23 23:42:25 +10:00
bobloy
fc4703ef80 [Launcher] Don't ask for CLI flags when no instances (#2142)
Seems to have been fixed in #1497 but accidentally reverted at a later date.
2018-09-23 23:39:32 +10:00
zephyrkul
a301b2c758 Add missing @staticmethod to Red.send_filtered (#2143)
Method was previously missing `self` argument but was missing static decorator.
2018-09-23 23:34:57 +10:00
Toby Harradine
e27682abd3 [Downloader] More robust repo loading (#2121)
Previously, when downloader was loaded, the RepoManager would spawn a task to load available repos. If one repo failed loading for some reason, the function would raise and the remaining repos would never be loaded, however downloader would still appear to load correctly.

This change handles exceptions better during repo loading, but also, if an unhandled exception is raised, downloader will fail to load as it should.

Also included, as requested in #1968, is the --recurse-submodules flag in cloning/pulling repositories.

This change resolves #1950.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-09-22 15:05:42 +10:00
Toby Harradine
df922a0e3e [Audio] More robust startup and unload (#2118)
* More robust cleanup for audio and streams

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

* Remove copied code from streams

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-09-22 15:04:46 +10:00
aikaterna
51c83d958c [Audio] Check if player is playing when pausing (#2130) 2018-09-18 23:54:32 +10:00
Toby Harradine
17139ce439 Fix compiler detection on Windows (#2136)
Signed-off-by: Toby Harradine <t.harradine@student.unsw.edu.au>
2018-09-18 14:07:50 +10:00
zephyrkul
61652a0306 [V3 CustomCommands] Cooldowns (#2124)
* customcom cooldowns

allows you to set multiple different cooldowns for custom commands

* black formatting

* [docs] cooldowns
2018-09-17 17:47:45 +02:00
Michael H
113b97b9c9 Fix role check in local blacklist/whitelist (#2134)
fixes #2133 , 

`discord.Role.is_default` is a method, not a property.
2018-09-17 17:30:56 +10:00
zephyrkul
2784730f7f [V3] Utilize shorten_by (#2131)
* utilize shorten_by

otherwise you get errors

* trivia shorten_by

* warnings shorten_by

max username length is 32 characters

* black formatting
2018-09-15 01:48:31 +02:00
Toby Harradine
1a9216b522 Set 3.6.6 as minimum python version on Windows (#2117)
* Set 3.6.6 as minimum python version on Windows

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

* Conditional python_requires in setup.py

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

* Should probably add the comment too

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-09-14 21:22:43 +10:00
Toby Harradine
08fc732b7b [Downloader] Fix URL parsing for non-GitHub/GitLab links (#2123)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-09-12 11:01:54 +10:00
Toby Harradine
54baf687ec [Downloader] Parse tree URLs when cloning repos (#2119)
* [Downloader] Parse tree URLs when cloning repos

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

* Only match GitHub and GitLab URLs

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-09-12 10:03:15 +10:00
Toby Harradine
8096cd803e Bump version to 3.0.0b21 (#2115)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-09-10 00:44:11 +10:00
Toby Harradine
27b0d606c8 Fix CCs with no args (#2114)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-09-10 00:35:53 +10:00
aikaterna
af220e497f [Docs] Update sample cog (#2072)
* [V3 Docs] Update sample cog

* Remove self.bot assignment

We don't really do this any more unless we need to
2018-09-09 23:35:22 +10:00
Toby Harradine
892b2487f5 Dependency update (#2100)
aiohttp-json-rpc 0.11 -> 0.11.1
atomicwrites 1.1.5 -> 1.2.1
attrs 18.1.0 -> 18.2.0
certifi 2018.4.16 -> 2018.8.24
discord.py 8ccb98d395537b1c9acc187e1647dfdd07bb831b -> 00a659c6526b2445162b52eaf970adbd22c6d35d
fuzzywuzzy 0.16.0 -> 0.17.0
imagesize 1.0.0 -> 1.1.0
multidict 4.3.1 -> 4.4.0
py 1.5.4 -> 1.6.0
pytest 3.7.0 -> 3.7.4
sphinx 1.7.6 -> 1.7.8

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-09-09 23:21:48 +10:00
Kowlin
7971c02dc5 Escape user and passwords for mongo (#2111)
* Escape user and passwords for mongo.

* Quote -> Quote_Plus

* formatting
2018-09-09 23:07:40 +10:00
zephyrkul
c1d8272b49 [CustomCommands] Show parameter name correctly in help (#2102) 2018-09-09 23:05:33 +10:00
aikaterna
bce5dd26f3 [Audio] Local playback (#2097)
* [V3 Audio] Local playback

* Formatting

* Update application.yml

* Add catch for an empty localtracks directory

* Linebreak in help for i18n
2018-09-09 23:02:53 +10:00
zephyrkul
04e97f3516 [CustomCom] Custom Command Parameters (#2051)
* [V3 CustomCom] Custom Command Parameters

Allows specifying more parameters for CC's via {0}, {1}, etc. that will be filled by the user invoking the CC. Python-style type hinting and attribute access is also allowed for Discord and builtin types.

> [p]cc add simple greet Hi, {0.mention:Member}!
> ...
> [p]greet zephyrkul
> Hi, @zephyrkul!

The bot will reply with the standard help messages if the cc is incorrectly executed.

> [p]greet me
> Member "me" not found

* black formatting

* check command failure

Only call the custom command if the faked command succeeded.

* misc fixes

1) don't str.strip all the time, it's not family-friendly and doesn't match transform_parameter
2) transform_arg now actually returns strings in every case
3) improve prepare_args parsing security
4) help parameters will show what type they expect
5) make linter less angery

* customcom documentation

I hate rst

* don't require repeated type hinting

If a parameter was type hinted previously, don't require it again.
Ex: `{0.display_name:Member}#{0.discriminator}` is now possible.

* add cog_customcom.rts to index

I despise rst

* don't enforce order

Allow type hinting and attribute access to be in either order.
Ex. `{0:Member.mention}` is now valid.

* clean up on_message

We're building context anyway, may as well use it.

* [doc] correct cog name

Cog class is named CustomCommands, not CustomCom

* minor on_message optimization

only build context if it's needed

* update cc_add docstring

Old one wasn't user-friendly. Replaced with a link to the new docs.
Link will not function until PR is merged and docs refreshed.

* [doc] change repeat to say

repeat is an audio command, use say in the example instead

* compare ctx.prefix to None

allows for null prefixes, which is a bad idea but who am I to judge

* address review

* raise error on conflicting colon notation

bugfix I was working on but failed to actually commit
2018-09-07 00:14:02 +10:00
Toby Harradine
7eed033c9e Prettify README and translate to MarkDown (#2101)
Signed-off-by: Redjumpman <redjumpman@users.noreply.github.com>
2018-09-06 21:53:18 +10:00
Richard Guan
a2fe1a8757 [Audio] Don't allow duplicate links in playlists (#2071)
* Just a test

* another test

* undoing my test

* First solution, style issue

* Works functionally, but there are is a minor style issue

* style fixed

* Revert README changes
2018-09-06 16:25:27 +10:00
Toby Harradine
9ee860c3f0 [Core] Command disable feature (#2099)
* [Core] Command disable feature

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

* [Core] Allow user to set the "command disabled" message

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

* Reformat

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-09-06 12:52:19 +10:00
Toby Harradine
1dbe9537e9 Ignore fuzzywuzzy slow sequence matcher warning (#2096)
* Ignore fuzzywuzzy slow sequence matcher warning

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

* Add log.info call instead

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-09-06 12:50:30 +10:00
Toby Harradine
775f4ce69a Use ProactorEventLoop on Windows (#2062)
* Use ProactorEventLoop on Windows

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

* Set the actual loop instead of the policy

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-09-06 00:40:15 +10:00
zephyrkul
e83beeef34 [Audio] Add [p]sing (#2067)
* port [p]sing

* [p]sing in alphabetical order

what even is an alphabet
2018-09-04 13:23:50 +10:00
Brramble
e77cfff892 [p]serverinfo respects set embed colour (#2083) 2018-09-04 13:01:50 +10:00
Michael H
9495432b8f More mention filtering (#2081)
* more filters

* note to future people touching this

* chan mentions were almost missed...

* Swap strategies as the previous escaped the mention, while still pinging the user/role
2018-09-04 12:59:59 +10:00
palmtree5
d71c334a34 [Filter] Fix issue with filtering sentences (#2065) 2018-09-04 12:55:18 +10:00
Michael H
aa8cc90ad0 Update minimum Python version to 3.6.2 (#2093)
* Correct minimum version

see #2092 

While this is needed because of an import just for typing, I see no reason not to bump the minimum version since this is a minor version difference since this is several minor version behind the latest 3.6, and there have been both security and performance improvements since.

That said, we need to be testing on our lowest supported version to ensure we don't have this happen again, right now our tests run on whatever Travis grabs for 3.6, which I assume is 3.6.6, but could be wrong.

* Update other mentions of min version to 3.6.2
2018-09-04 11:38:56 +10:00
palmtree5
589041556e [Streams] Fix excessive writes to config (#2095)
Resolves #2052.
2018-09-04 11:01:48 +10:00
El Laggron
85354f2722 Support missing .po files (#2068)
* [V3 Core] Support missing .po files

* Modified if-state

* Use PEP8 recommendation for empty list check

https://www.python.org/dev/peps/pep-0008/#programming-recommendations
2018-09-03 15:44:58 +10:00
Toby Harradine
c0c5535005 [Warnings] Help users understand custom reasons (#2049)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-09-03 14:57:14 +10:00
zephyrkul
e126cf9f4e [Launcher] use "python -m" to run Red (#2080)
friendlier for virtual environments
2018-09-03 10:44:50 +10:00
aikaterna
c2d23b37a7 [Audio] Fix for using [p]now with thumbnail (#2063) 2018-08-28 13:02:40 +10:00
aikaterna
3a968d707f Fix Travis deployment attempt 2 (#2061) 2018-08-27 10:24:03 +10:00
aikaterna
b07c44c8b4 Fix travis deployment (#2060) 2018-08-27 10:09:02 +10:00
aikaterna
43b0a58649 [Audio] Fix for embed color (no, COLOUR) on notify messages (#2059) 2018-08-27 09:44:19 +10:00
Toby Harradine
f258e93cf7 Bump version to 3.0.0b20 (#2050)
Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
2018-08-27 09:28:37 +10:00
Toby Harradine
93138b04cb [Streams] Set YouTube channel name when added by ID (#2047)
* [Streams] Set YouTube channel name when added by ID

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

* Move unset token raise to correct place

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

* Correct logic in get_stream

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

* Fetch name explicitly instead

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

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

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

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

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

* [Docs] Better cross-referencing

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

* Fix pyenv-installer link

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

* Use sudo -e instead of sudo nano

* Add note about launcher for linux/mac

* Include launcher notes in Windows guide

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

Removes red-trivia as a dependency.

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

* Include package data in distribution

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

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

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

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

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

* Clear out autogenerated `messages.pot` files

* Remove redundant `regen_messages.py` files

* Refactor `generate_strings.py` to use redgettext

* Install redgettext in Travis Crowdin job

* Clean up some problematic usages of gettext function

* Reformat

* Replace generate_strings.py with Makefile argument

* Update to redgettext 2.1, use exclusion pattern

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

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

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

* Fix unintended side-effects of new behaviour

* Add tests

* Add test for get_raw mixing in defaults

* Another cleanup for relying on old behaviour internally

* Fix bank relying on old behaviour

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

* Add some tests

* Fix docs reference

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

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

* Formatting

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

* [V3 Audio] Add thumbnail to notify messages

* Formatting

* Update thumbnail fetching

* Update thumbnail fetching

* Track thumbnail moved to Red-Lavalink

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

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

* Import timedelta...

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

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

* Better handling of the sys arguments

* Black reformat to -l 99

* Update launcher to PR#2025

* Always append --user if not in a virtualenv

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

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

This reverts commit 7959654dc8.

* refactor cleanup

* formatting pass

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

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

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

* Requested changes + wrap as_completed instead

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

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

* Remove requirements.txt from tox.ini

* Update and pin all dependencies and sub-dependencies

* Update for breaking changes

* Reformat

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

* Add 3.7 to identifiers and travis/tox builds

* Attempt at fixing the travis build matrix

* Attempt #2

* Attempt 3

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

* Add raven-aiohttp to requirements

* Fix stuff in setup.py

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

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

* Update to Rapptz/discord.py@8ccb98d395

* Add proper 3.7 build in Travis

See travis-ci/travis-ci#9815

* Which version of 3.6 does Xenial install then?

* Maybe we should stop pipenv installing useless stuff

* Nevermind, back to specific minor version

* Remove lots of WET dependency stuff

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

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

* clearer user feedback

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

* more detailed error with docs link + docs update

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

* Reformat

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

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

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

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

Resolves #1982.

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

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

* Make the core path a class attribute

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

If not specified, it will be the latest case.

* Fix some errors

* Black reformat

* More info for usage string

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

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

* Add a test

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

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

* Used black for formatting

* Fixed everything according to Tobotimus's review

* Fixed the description limit to 2048 characters

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

* Add non-embed version

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

* Update __main__.py

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

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

And some sanitizing of playlist names.

* [V3 Audio] Playlist naming standardization

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

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

* Update reports.py

minor change for clarity

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

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

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

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

* Update mod.py

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

* [V3 Audio] Respect the user limit

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

w/ Black if I did this right

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

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

* cache fix

* smarter cache invalidation

* One more cache case

* Put in a bare skeleton of something else still needed

* more logic handling improvements

* more work, still not finished

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

* small bugfixin + comments

* add note about before/after hooks

* LRU-dict fix

* when making comments about optimizations, provide historical context

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

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

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

* ...

* ...

* ...

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

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

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

Also made some lines a bit shorter

* A few more

* Fix typo

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

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

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

* --no-instance flag

* Reformatted cli with black

* Fix changes requested by @Tobotimus

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

* Update create_temp_config call

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

* Update docs

* Let's do the same for repo addition

* Fix indent

* Use replace instead of format

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

* Fix syntax

* Fix typo

* Make text underlined

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

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

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

* Add core dunder all

* Update other dunder all's

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

Prevents trying to do a string replace on a NoneType

* address root cause as well

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

* Add a test

* foo not bar

* Add a test for setting and then mutating

* Resolve issue for setting and mutating as well

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

* Update docs requirements

* Black reformat after version update

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

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

* Add a test for repo removal

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

* [V3 Audio] Small fixes

* [V3 Audio] Remove unused variable

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

* Add dataconverter tests file

* Fix past nicks spec converter

* Update gitignore for dataconverter data files

* Add data file for dataconverter test

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

* Replace existing `discord.ext.commands` imports

Just to be sure

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

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

* black format pass

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

* help formatter pagination fix

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

* black format

* add autohelp

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

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

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

* Implement RPC handling for core

* Black reformat

* Fix docs for build on travis

* Modify RPC to use a Cog base class

* Refactor rpc server reference as global

* Handle cogbase unload method

* Add an init call to handle mutable base attributes

* Move RPC server reference back to the bot object

* Remove unused import

* Add tests for rpc method add/removal

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

* Add one more test

* Black reformat

* Add RPC mixin...fix MRO

* Correct internal rpc method names

* Add rpc test html file for debugging/example purposes

* Add documentation

* Add get_method_info

* Update docs with an example RPC call specifying parameter formatting

* Make rpc methods UPPER

* Black reformat

* Fix doc example

* Modify this to match new method naming convention

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

* prevent heirarchy issues in mod

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

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

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

* More quote cleanup that I missed

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

* [V3 Warnings] Change allowcustomreasons docstring

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

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

* Missing await

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

This includes a role listing

* format pass

* Update core_commands.py

* .

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

* Make it check if parent commands are hidden

* Check if compiler available in setup.py

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

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

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

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

Also removes a faulty assumption about not needing cleanup tasks

* Update __main__.py

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

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

* Log download of Lavalink.jar event

* Fix #1709's other bug

* Reformat

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

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

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

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

* Add a few more commands

* Refactor load/unload signature

* Add invite URL and version info

* Black fixes

* Split the incoming cog names in reload correctly

* Reformat

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

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

* Update filter.py

* Update permissions.py

* Update customcom.py

* Update image.py

* Update trivia.py

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

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

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

* twine

* twine

* twine
2018-05-28 19:03:50 +02:00
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
palmtree5
501aff41ea [V3] Bump version to 3.0.0b14 (#1629) 2018-05-13 16:24:40 -08:00
Michael H
449b1bfe9e Make checks.py manageable from a permissions cog (#1547)
* This starts setting up checks.py to handle managed permission overrides

* missing else fix

* don't bypass is_owner, ever

* 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

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

* un mix the 2 PRs (*sigh*)

* Update checks.py

remove debug prints that got lost inshuffle
2018-05-14 10:13:16 +10:00
aikaterna
4a8358ecb4 [V3 Audio] Update queue and search to use menus (#1633)
* [V3 Audio] Update queue and search to use menus

* [V3 Audio] Fix for playlist upload saving

* [V3 Audio] Add position in queue to enqueued songs

Also a bit of cleanup.

* [V3 Audio] Improvements for mobile formatting
2018-05-14 10:01:46 +10:00
Tobotimus
8f74e4dd31 [V3 Cleanup] Cleanup commands clean up after themselves (#1602)
Resolves #1572
2018-05-13 15:51:50 -08:00
Michael H
2b35d9f012 [V3 cleanup] Respect pinned messages by default (#1596)
* This sets the default behavior for `get_messages_for_deletetion()` to not include pinned messages, while providing a way to override that

resolves #1589

* actually make commands parse for pinned deletion

* fix capitalization
2018-05-13 15:49:45 -08:00
palmtree5
35001107e0 [V3 Streams] cache stream alert messages across restarts (#1630)
* [V3 Streams] cache stream alert messages across restarts

* Add some stuff to debug this

* More debug stuff

* More debug stuff

* Actually save when updating a stream alert

* Remove debug stuff

Fixes #1620
2018-05-14 09:42:28 +10:00
Leo Garcia
a7d7b90ae8 [V3] Removed py 3.6 warning for Windows (#1622)
I believe we've fixed this awhile ago.
2018-05-14 09:32:41 +10:00
Tobotimus
119ba7ef8b [V3 ModLog] Fix [p]reason when the modlog case has no moderator (#1604) 2018-05-14 09:24:17 +10:00
palmtree5
28bbe9c646 [V3 i18n] add a NoneType check on trying to normalize a string (#1632)
Fixes #1631
2018-05-14 09:10:38 +10:00
Michael H
8739c04024 [V3] Ping changes (#1618)
* moves ping to core commands
defaults ping behavior to reacting with a ping pong paddle with ball
adds an optional boolean flag to ping to get the avg latency from the bot
(strikes a middle ground with intended behavior from dev standpoint, and how users want it)

* casing for @Kowlin

* use correct check for permissions

* remove latency
2018-05-13 15:03:17 -08:00
Tobotimus
57240d25b9 [V3] Update trivia version and allow installing in develop mode (#1635)
* [V3 Trivia] Update trivia version to >1.1

* Use actually working trivia version
2018-05-13 13:43:16 -08:00
Tobotimus
15ea5440a3 [V3 i18n] Internationalise help for commands and cogs (#1143)
* Framework for internationalised command help

* Translator for class docstring of cog

* Remove references to old context module

* Use CogManagerUI as PoC

* Replace all references to RedContext

* Rename CogI18n object to avoid confusion

* Update docs

* Update i18n docs.

* Store translators in list instead of dict

* Change commands module to package, updated refs in cogs

* Updated docs and more references in cogs

* Resolve syntax error

* Update from merge
2018-05-12 01:47:49 +02:00
Michael H
1e60d1c265 [V3] adds a permissions check for embed_links in ctx.embed_requested (#1619) 2018-05-10 14:35:18 -08:00
Tobotimus
b7cd097c43 [V3 Trivia] Lock trivia version to <1.1 (#1621) 2018-05-10 13:20:40 -08:00
bobloy
6c934b02e6 [V3] Fix help's help (#1606) 2018-05-10 13:14:44 -08:00
Kowlin
fcb9b40b43 [V3] Fixed [p]servers bug (#1617)
* Fixed servers bug

* Added protections against going negative
2018-05-10 13:10:42 -08:00
Michael H
7a6884e4b1 [V3] Mark 3.7 as unsupported in setup.py (#1623) 2018-05-10 13:04:20 -08:00
Michael H
e86698cfeb [V3] Update some user facing info (remove old, outdated info) (#1613)
* remove outdated link in favor of in docstring docsumentation

* Update default Downloader repo url to org repo url (don't rely on github redirect)
2018-05-08 22:27:38 +02:00
Bakersbakebread
53650aefa6 [Docs] Added self (#1608) 2018-05-08 19:47:11 +02:00
Tobotimus
1d80a0cad1 [V3 Mod] Fix issue with unmuting, again (#1603)
* [V3 Mod] Fix issue with unmuting, again

Resolves #1595

* Fix typo
2018-05-07 13:31:14 +02:00
retke
f6d27a0f43 [V3 Parser] Added --load-cogs flag (#1601)
* [V3 Parser] Added --load-cogs flag

* Removed old PR data

* Removed old PR data

* Removed old PR data

* Slightly reword help for flag

* Stick to convention for checking if sequence is empty

* Fix some logic errors

* Don't print packages which failed to load
2018-05-07 15:01:44 +10:00
Wyn
f71aa9dd21 [V3 Docs] Autostart (#1599)
Moved note to the top, added how to access red log.
2018-05-05 15:43:26 -08:00
palmtree5
1cb5394e96 [V3] bump version to 3.0.0b13 (#1583) 2018-05-04 08:48:33 +02:00
palmtree5
2b2dbd25f7 [V3 Help] fix issue with non-existent subcommands (#1565) 2018-05-03 22:19:24 -08:00
Michael H
dd4cd0eeb1 provide an extra method for helping wor with embed_requested (#1558) 2018-05-04 08:16:24 +02:00
palmtree5
ee7b0cf730 [V3 Utils] fix files not being chmodded (#1578) 2018-05-04 08:10:56 +02:00
retke
95ef5d6348 [V3 Launcher] Reinstall Red option (#1536)
* [V3 Launcher] Reinstall Red option

* [V3 Setup] Divided remove_instance function

* Removing changes from another PR

* Indent fails fix

* use remove_instance_interaction for --delete

* Fix some issues with remove_instance

removed `index: int` because what's being passed there is a string
data -> instance_data

* bug fixes, working version
2018-05-04 08:01:37 +02:00
bobloy
23192b9ef6 simple_embed doesn't take author (#1555)
Simple embed doesn't use ctx.author as author
2018-05-04 07:27:44 +02:00
Michael H
7cd98c8a63 Report fixes + improvements (#1541)
* WIP

* fix perms issue

* better

* more work

* working

* working, tessted

* docs

* mutable default fix
2018-05-04 06:38:58 +02:00
palmtree5
fca7686701 [V3 Core] fix 3.5-specific issue with [p]backup (#1586) (#1588) 2018-05-04 06:18:44 +02:00
Michael H
be767478f4 allow deletion based on user ID (actually this time) (#1561) 2018-05-04 06:15:27 +02:00
palmtree5
b3ad5d90ed [V3 Core] fix a couple issues with [p]servers (#1580) 2018-05-04 06:05:09 +02:00
palmtree5
fb093b7411 [V3 Utils] Menu system (#1566)
* [V3 Utils] start on a menu system

* Fix conflicting names

* [V3 Menus] change order of default controls

* [V3 Menus] add a message check to the react check

* Add a note about original source and who ported

* Compare message ids, not the objects themselves
2018-05-04 05:54:30 +02:00
palmtree5
e4ea3110e3 [V3 Warnings] fix several bugs found (#1577) 2018-05-04 05:46:59 +02:00
aikaterna
79676c4f72 Playlist additions and cleanup (#1579)
Add playlist append, create, remove, and upload.
2018-05-04 05:43:00 +02:00
Wyn
d61827b92c [V3 Docs] Fixed broken link (#1567)
Guide migration went into a maze, this should fix it.
2018-05-04 05:33:01 +02:00
palmtree5
1f1f46c70f [V3] move to multiple issue/pr templates (#1585) 2018-05-04 03:58:30 +02:00
Wyn
9188e4a7ec [V3 Info] Don't rely on redirect (#1581)
* [V3 Info] Don't rely on redirect

Http -> Https

* Update core_commands.py

Use existing variable instead of new string

* Update events.py

Remove redirect, url only reference
2018-05-02 10:35:48 +02:00
Redjumpman
e5a780eb0c Update mod.py (#1582)
Update doc-strings to properly format in the help text.
2018-05-01 09:13:25 +02:00
palmtree5
d8c85a2b15 [V3 Audio] fix zombie process on unload (#1575) 2018-04-29 08:19:49 +02:00
palmtree5
83080bc5a2 [V3 Mod] fix issue with unmuting (#1568) 2018-04-28 13:39:06 +10:00
Wyn
233bfc59ac [V3 Docs Arch] Upgrade dependencies (#1553)
-u parameter added to pacman for upgrading dependencies so we don't get partial upgrades.
2018-04-19 13:29:44 -08:00
Bakersbakebread
c606caf3a3 Grammar Change With -> Will (#1539)
Any message successfully forward WILL be marked...
2018-04-18 12:28:12 +02:00
palmtree5
efdf69552f [V3] Beta 12 release (#1525)
* [V3] update translations from Crowdin

* [V3] bump version to 3.0.0b12
2018-04-16 19:05:21 -08:00
Michael H
38531bf95c [V3 Tunnel utils] Better handling of tunnel instances (#1538)
* Better handling of tunnel instances

* docs
2018-04-16 19:02:38 -08:00
Wyn
6d714db928 [V3 Audio] Add Lavalink Jar build to settings (#1430)
* [V3 Audio] Add Lavalink Jar build to settings

Added Lavalink Jar build to Lavalink settings since build number now stored in config.

* Forgot import

Added import

* Left align

K im done for real this time
2018-04-16 18:57:17 -08:00
Michael H
f8e7497695 [V3] Remove old usage of type tuples with config (#1518) 2018-04-16 21:41:13 -04:00
TheFixed
d6ef812704 [V3 Admin] fix typo (#1537)
it's -> it is or it has
its -> possessive form of it
2018-04-16 17:36:38 -08:00
Will
4f81bc9621 [V3 CogManager] Fix duplicate paths/reorderpath not working (#1501)
* Fix duplicate paths/reorderpath not working

* Make remove path use the visible paths not the internals.
2018-04-16 21:21:31 -04:00
aikaterna
59276ce2a5 [V3 Audio] External lavalink server settings, playlist saving & recall, bugfixes (#1528)
* Add settings for external lavalink servers

* Add external lavalink server to settings display

* Add simple error handling

* Remove future permissions conflict

Assuming base Red permissions will be accessed with the 'p' command in the future

* Update prev to set requester to user instead of id

* Various fixes

Jukebox: Plebs not in a channel will not have the song cost deducted before the warning to join a channel first.
DJ role: Finer grained permissions for plebs that are alone: they can use prev (if shuffle is off), skip, pause, resume

* Update permissions

* Add track number to footer when queueing

* Add basic playlist saving

More subcommands to come later

* Check if user is not in voice

* Restrict playlist deletion to author and mods

* Add playlist info command
2018-04-17 00:42:32 +02:00
Michael H
96791bd72b [V3] supress pynacl warning (#1508)
* supress pynacl warning

* move the supression to __init__.py

* ..

* Revert "move the supression to __init__.py"

This reverts commit ee7ba9c968.

Doesn't properly work here

* Changes are fine in __init__.py

Needs a module level import in __main__.py or the console scripts generated by pip setup don't ever run the code in __init__

* supress pynacl warning

* puts the changes back again to avoid duplicate module import warning
2018-04-16 14:32:05 -08:00
Michael H
8756b22f5a [V3] Report Tool (#1281)
* Okay, let's fix the issues here hopefully.

* This is working now

* Unfinished, and needs a lot of testing.

* more work

* working

* minor thing to remove

* improve i18n and usage feedback
2018-04-16 13:56:14 -08:00
Wyn
487d256f46 [V3 Docs] Added Arch Installation guide (#1504)
* Added Arch Installation guide

For prereqs:
python-pip pulls python which pulls required packages libffi and openssl
Removed ffmpeg for Java replacement

* index

Adding the guide to the index sounds like a smart idea...

* Removed libsodium

Not needed after all

* added --user flag to installs for pip
2018-04-16 23:45:53 +02:00
Pier-Angelo Gaetani
fe0b6aaba4 [V3 Docs] Systemd guide + extras (#1505)
* Documentation extras

Added links to each distro's user add functions
Added systemd service guide
Added --user operator to pip install commands

* Update autostart_systemd.rst

Added no-prompt flag
2018-04-16 23:44:08 +02:00
retke
017c8a6900 [V3 Mod] Denying add_reactions permission to muted users (#1495)
* denied permission of adding reactions to muted users

* Track both send_messages and add_reactions perms
2018-04-16 12:17:03 -08:00
Michael H
afe4af0dc2 [V3 Launcher] fix issue with instance removal via launcher (#1535)
* handle issue

* or it would have if I had put it on the right line

* no more making patches from my phone
2018-04-16 11:57:57 -08:00
bobloy
fb8e4430ac [V3] Fix Cleanup loop (#1533)
* prevent infinite loop

* pep8 updates

Assorted pep8 updates to retrigger Travis

* Revert "pep8 updates"

This reverts commit d0c7f8b808.
2018-04-16 11:51:01 -08:00
Bakersbakebread
7499f5dbfa [V3 Mod utils] Beihnd -> behind typo (#1531)
Fixed typo
2018-04-16 11:41:11 -08:00
bobloy
c7b58aa65b [V3 Economy] guild -> server in [p]bank reset (#1530) 2018-04-16 11:37:29 -08:00
bobloy
705d9b8238 [V3] guild->server (#1529)
Mostly string renames
customcom now recognized {guild} and {server}
2018-04-16 11:32:51 -08:00
bobloy
1bc650b0f9 [V3] Mod.py guild -> server (#1522)
* mod guild->server naming

* typo
2018-04-16 10:13:26 +02:00
palmtree5
580aeea4e2 [V3 Instance setup] fix an issue getting credentials to convert Mongo to JSON (#1514)
* [V3 Instance setup] fix an issue getting credentials to convert Mongo to JSON

* Attempt a fix at data conversion issues
2018-04-16 10:03:21 +02:00
palmtree5
8495824843 [V3 Core] unhide [p]embedset (#1523) 2018-04-16 09:59:34 +02:00
aikaterna
18bb3611fa [V3 Economy] Add credits name to payday (#1496) 2018-04-15 15:23:09 -08:00
bobloy
9f7bf8d1a2 [V3 Launcher] don't get cli flags if no instances are found (#1497)
Don't get cli flags unless there is an instance to get
2018-04-15 15:20:59 -08:00
Michael H
82146eda3d [V3/docs] New Audio, remove old reqs... (#1503)
* typo fix

* update docs with up to date reqs (remove old) and reccommend venv usage

* remove youtube_dl from reqs

* adding the mac doc update after verifying correctness

* venv installation required for py3.5 ...

* remove venv stuff, that can be handled on a unified page in a later PR
2018-04-15 14:44:16 -08:00
Sebass13
d75881e1a3 [V3 Economy] Add per-role payouts (#1488) 2018-04-15 14:40:32 -08:00
Will
5be967e8c5 [V3 Audio] Harden Lavalink boot sequence (#1498)
* Do a bit of hardening

* Loop not asyncio

* Don't use new asyncio coolness

* I hate you all

* Muck up everything

* Fix version comparisons
2018-04-15 14:01:56 -08:00
Kowlin
d9fa875d84 [V3] Splitted deployment stage into their own stages. (#1524)
* Splitted deplyoment stages into their own stage.

* Updated Codeowners to reflect meta files.
2018-04-13 13:16:36 -08:00
rngesus-wept
04f93c98d2 [V3] Add .pytest_cache to .gitignore (#1520) 2018-04-11 14:45:53 -08:00
palmtree5
402f6c19e7 [V3 Core] fix some issues with [p]set and [p]set nickname (#1494)
* [V3 Core] fix AttributeError in [p]set

* Fix [p]set nickname

* Make the nickname param on [p]set nickname optional
2018-04-04 12:42:10 +02:00
Michael H
84b0df0437 [V3 Data Converter] Python3.5 compatibility issue patch (#1491)
* python3.5 patch

* urf -> utf
2018-04-02 22:17:40 -04:00
palmtree5
9f4a7f7623 [V3] Bump version to 3.0.0b11 (#1483) 2018-04-02 17:19:50 -08:00
retke
10012c1e16 [V3] added --version flag (#1479) 2018-04-02 17:17:19 -08:00
Michael H
d65f8856f4 [V3] Data Converter (#1293)
* More docstrings

* still not ready...

* push this untested pile of code.

* working, menu needs cleaning up though, modlog converter not here yet

* menu cleanup

* add note about the fact that values are overwritten

* add i18n

* User friendlier quitting

* Better naming of a function

* setup automodule for dataconverter

* More documentation

* use Config.MEMBER (etc) instead of 'MEMBER' (etc)
2018-04-02 21:14:37 -04:00
Will
d79d8fbbea [V3] Fix logic issue with [p]load (#1460) 2018-04-02 17:09:17 -08:00
palmtree5
772590c41c [V3] Add settings displays to [p]set and [p]bankset (#1475) 2018-04-02 21:01:43 -04:00
aikaterna
d9c71bddb8 [V3 Audio] Add voteskip and DJ role (#1473)
* [V3 Audio] Add voteskip and DJ role

* Fix connection time storage for audiostats

* Update pause command for more explicit handling

Partially fixes #1478. Users will need to use pause twice before resuming until issue no. 20 on the Red-Lavalink repo is addressed.

* Update pause command

* Update pause command

* Add jukebox mode

Adds a price option for enqueuing songs. If DJ mode is active when jukebox mode is active, no one is charged credits as mods are exempt and plebs can't queue songs without the DJ role.
2018-04-02 20:53:37 -04:00
Will
29ce2401ca [V3 Config] Fix unloading and implement singleton driver (#1458)
* Add the identifier as an initialization parameter

* Remove config object singleton and opt for a shared JSON datastore

* Fix bot unloading to deal with memory leaks

* Fix tests

* Fix clear all bug
2018-04-02 16:47:27 -08:00
Will
720ef38886 [V3 Help] Fix the two missing characters in help (#1456) 2018-04-02 16:29:44 -08:00
Will
728ab6c8c1 [V3 Downloader] Allow for requiring minimum python version (#1455) 2018-04-02 16:26:07 -08:00
Will
935028addc [V3 Downloader] Add install messages from info.json (#1453) 2018-04-02 16:23:55 -08:00
palmtree5
e70e22d557 [V3 Image] Actually check if a client ID is set before making request (#1485)
* [V3 Image] actually check if a client id is set before making request

* Clarify imgurcreds docstring + drop guild_only from [p]imgur

* Fix the actual problem (bad urls)

* needs to be params not data
2018-04-02 20:14:16 -04:00
palmtree5
f3db4b5cb0 [V3 Modlog] Fix TypeError related to 'until' (#1487) 2018-04-02 20:09:09 -04:00
palmtree5
d60fc06379 [V3] Update translations from Crowdin (#1490) 2018-04-02 19:39:07 -04:00
Wyn
a813260717 [V3 Audio] Bump timeout connecting to Lavalink (#1474)
* [V3 Audio] Bump timeout connecting to Lavalink

Lavalink server can take 20-35 seconds to start, bumped to 50 seconds for good measure.

* Update audio.py

Added proper timeout

* Update __init__.py

Removed old timeout
2018-04-02 19:36:26 -04:00
bobloy
10d01df7dd [V3 General] Text fix in flip command (#1489) 2018-04-02 12:43:11 -08:00
palmtree5
8eb075fa86 [V3 Core] Embed toggling support (#1467)
* [V3 CogManagerUI] implement text-only support in [p]cogs

* Change page length to 1800 + workaround for pages starting with delim

* [V3 Core] text mode support for [p]contact and [p]dm

* Implement text-only support in modlog and fix everything that broke because of it

* Fix modlog stuff in filter too

* Fix tests

* Implement optional embed support in [p]help

* move to .format
2018-03-28 20:15:51 -04:00
palmtree5
c2e7bfa641 [V3 Instance setup] Implement backup support for instances using Mongo (#1470)
* [V3 Instance setup] import backup support for instances using Mongo

* remove_instance is now a coroutine + remove NIE

* move Mongo import down

* move another import
2018-03-28 20:12:26 -04:00
palmtree5
2f23244937 [V3 Core] Add [p]backup (#1471)
* [V3 Core] add [p]backup

* move imports
2018-03-28 19:53:27 -04:00
palmtree5
9ecea9e1d5 [V3 instance setup] Warn and request confirmation if instance name already in use (#1469) 2018-03-28 19:13:34 -04:00
palmtree5
61b34e835e [V3] Drop an unnecessary if in embed_requested (#1468) 2018-03-28 19:10:02 -04:00
palmtree5
4e2b83c052 [V3 Docs] Add and update existing features (#1451)
* [V3 Docs] bump version

* Remove removed functions from __all__

* Update docstring format

* Update copyright year

* Add embed utils docs

* Document bot class

* Document RPC

* Update toctree
2018-03-24 17:27:04 -04:00
palmtree5
45ee4755a6 [V3 Core] Fix url sent when doing [p]invite (#1461) 2018-03-24 17:24:06 -04:00
palmtree5
59281afe90 [V3 Instance setup] cog identifiers are strings, not ints (#1448) 2018-03-22 19:11:27 -04:00
retke
cf5a63a5d5 [V3] Set token command fix (#1449)
* [V3] Add set token command

* DM check

* warning message + delete if token isn't send in DM

* Update string formatting

* fix condition invertion
2018-03-22 19:08:57 -04:00
Will
96869074c3 [V3 Docs] Third time's the charm (#1447)
* Get the right sphinx version

* Add mongo extra
2018-03-20 21:14:25 -04:00
Will
c107d5fa7f [V3 Docs] Really fix it this time (#1444) 2018-03-20 21:08:01 -04:00
Will
bed56e8891 [V3 Docs] Add new RTD yaml file (#1443) 2018-03-20 20:58:48 -04:00
palmtree5
c4b641e62a [V3] Bump version to 3.0.0b10 (#1418) 2018-03-20 16:36:32 -08:00
James
153d710eb4 [V3] Allow load, unload and reload to deal with multiple packages (#1441)
* [V3] Allow load, unload and reload to deal with multiple packages

This PR allows multiple packages to be loaded, unloaded or reloaded with the one command, the packages are delimited by the space character as suggested by Will
This is just the functionality, I'm sure the code could be better

* introduced helper function for getting package load strings

* missed characters

* forgotten import 👀

Forgot to include the import `inline` method from chat_formatting
2018-03-20 16:32:54 -08:00
palmtree5
83471e0866 [V3 Core] implement commands and settings for embeds (#1437)
* [V3 Core] add settings for whether to use embeds or not

* Implement commands to toggle embed settings

* Add a function to context for finding whether to use embeds or not

* Hide [p]embedset for now

* Move embed_requested to bot

* Add a simple helper in context
2018-03-21 01:17:40 +01:00
Michael H
01b9843883 [V3] Change presence (continued fixes) (#1438)
* This better fixes the root issue rather than attempting to work around it

* move bot_in_guild to checks, and use the correct syntax
2018-03-21 01:12:40 +01:00
retke
749af89e9f [V3] Set token command (#1425)
* [V3] Add set token command

* DM check

* warning message + delete if token isn't send in DM

* Update string formatting
2018-03-21 01:08:36 +01:00
palmtree5
27b61a2770 [V3 Streams] Fix community alerts (#1426)
* [V3 Streams] fix community alerts

* drop unneeded parentheses + delete messages when removing from cache

* move to one embed for a community

* fix adding image to embed

* Remove unneeded params

* Really helps to point to the right place

* drop the game, move channel name+link to value

* Use stream title in embeds

* Increase number of streams to grab to 100

* filter out streams with existing individual alerts in the channel

* need channel id, not stream id in filtering function

* sample from the list

* we only want to break out if the alert is a TwitchStream alert

* drop off sending the mentions if there are none
2018-03-20 19:25:23 -04:00
palmtree5
eb3b6346bb [V3] Set r/w access before deleting files (#1412)
* [V3] set access before deleting

* [V3] move+rename do_delete and use in repo removal in downloader
2018-03-20 18:46:15 -04:00
aikaterna
2e9a0de4a1 [V3 Audio] Update audio to use Red-Lavalink (#1440)
* [V3 Audio] Update audio for Red-Lavalink

* Update requirements for new library

* Fix logic for connect time storage for audiostats

Only store time when connecting to the voice channel.
2018-03-20 23:28:31 +01:00
palmtree5
f83e3cc3e7 [V3 Economy] Expand payday output (#1386)
* [V3 Economy] implement suggestions from #1371

* Fix a typo

* Add functions for getting leaderboard and leaderboard position

* Use the new functions to get leaderboard position and leaderboard (overrides https://github.com/Cog-Creators/Red-DiscordBot/pull/1435)

* Actually implement showing only guild members on leaderboard when bank is global

* get_leaderboard_position needs to be awaited

* For global bank, pass None for guild to get_leaderboard when trying to find position

* Remove some unneeded code

* Wrong index...

* Combine 3 messages into 1

* Fix guild leaderboard while bank is global

* add missing parentheses

* Modify the leaderboard formatting

* More work on leaderboard formatting

* no subtraction
2018-03-20 18:22:10 -04:00
palmtree5
a8f4659552 [V3 Instance Setup] Storage swapping (#1421)
* [V3 Instance Setup] start work on storage swapping

* This should do the trick for Mongo -> JSON

* Fix typo

* Fix a few more typos

* resolve the data path

* Upsert the imported data

* need a list of the documents

* to_list is a coro
2018-03-16 13:37:12 -08:00
Will
25a5c3dec9 Pip install to library dir (#1432) 2018-03-16 16:12:05 +01:00
Wyn
c49cb4a213 [V3 Readme] Update Patreon link (#1434)
Add on to #1431
2018-03-14 19:22:50 -04:00
palmtree5
c4dbbc2d1e [V3 Warnings] Action list isn't a dict (fix #1382) (#1408) 2018-03-14 19:10:07 -04:00
palmtree5
fe3d6f57af [V3 Streams] Add support for Youtube streams (#1385) 2018-03-14 19:07:14 -04:00
palmtree5
052af2f9bf [V3 Cleanup] Code consolidation (#1347)
* [V3 Cleanup] refactor to consolidate duplicate code

* [V3 Cleanup] make the prompting function a staticmethod

* Drop continuing message + add punctuation to docstrings
2018-03-14 19:03:11 -04:00
Adam
16da9f52ac [V3 Audio] update Lavalink build (#1427) 2018-03-12 21:48:13 -08:00
palmtree5
22a342d36d [V3 Bank/Economy] Fix #1404 and an issue with [p]bank reset (#1407)
* [V3 Bank] fix an issue with checks

* [V3 Economy] fix issues with [p]bank reset
2018-03-12 19:10:46 -04:00
palmtree5
4fcf32b5e9 [V3 Bank] Add confirmation prompt on [p]bankset toggleglobal (#1402) 2018-03-12 19:05:21 -04:00
palmtree5
5bdb455bc0 [V3 Instance setup] Change backup time separator for Windows (#1419) 2018-03-12 18:48:13 -04:00
BaIthamel
1cb74f0ea7 [V3] Fix for issue #1363 (#1424)
[p]cogs and [p]repo now print alphabetically
2018-03-12 18:44:30 -04:00
Tobotimus
c7e8c95640 [V3] discord.Game -> discord.Activity (#1397)
* [V3] discord.Game -> discord.Activity

* Update userinfo to reference new enum
2018-03-12 18:41:37 -04:00
Will
ccb322d08e [Audio] V3/auto autostart only (#1420)
* Download jar at audio load

* Messy...

* Remove leftover log file stuff

* Keep application.yml

* Damn you windows
2018-03-12 00:49:08 +01:00
Wyn
b27e0f2d21 [V3 Audio] Rename Game to Activity (#1409)
* Rename `Game` to `Activity` for audio

Add on to pull #1397

* Update audio.py

Whoops, don't need this here

* Update audio.py

Now fixed
2018-03-08 20:55:03 -05:00
aikaterna
6a715d87dd [V3 Audio] Add prev command, bugfixes for audiostats, search (#1405)
* Update audiostats to display days in timer

* Add prev command

* Update search button to play enqueued song
2018-03-08 20:50:54 -05:00
Tobotimus
6138b78c07 [V3 Admin] Make [p]announce consume all args into the message (#1394)
* Fix announce (resolves #1390)

* Make other commands consume all args

Also fixes [p]announce channel
2018-03-08 20:45:41 -05:00
palmtree5
f84ef48819 [V3 Docs] add java install to install docs (#1389) 2018-03-08 20:06:27 +01:00
palmtree5
cda27944b6 [V3 Mod] fix #1401 (#1403) 2018-03-07 09:48:22 +11:00
Will
c9281f734b [V3 Config] Fix clear throwing errors (#1374) 2018-03-06 10:28:37 +11:00
palmtree5
f378ea0d2e [V3 Core] add update check (#1388)
* [V3 Core] add update check

* [V3 Core] have it DM the owner if out of date
2018-03-06 10:21:01 +11:00
James
40c37b5c06 [V3 Help][ fix help_formatter to obey length limits (#1375)
Help didn't account for docstrings passing length limits which I noticed a while ago and as noticed again when Palm forgot a dual newline in a command docstring.
This PR sees to fix this by enforcing length limits on description, field names and field values
2018-03-06 10:10:56 +11:00
Will
57b7db6956 [V3 Admin] Remove guild default channel (#1381)
* Remove guild default channel

* Fix weird set thing
2018-03-06 09:52:26 +11:00
Michael H
b4f5c2c0a1 Revert "[V3]Encoding issue fix" (#1392)
* Revert "[V3 Core] Encoding issue fix (#1365)"

This reverts commit f6903cf582.

* Don't let the system encoding screw with things,
specify opens as happening with utf-8 encoding

(cherry picked from commit c10e4dddca)
2018-03-06 08:18:41 +11:00
Michael H
f6903cf582 [V3 Core] Encoding issue fix (#1365)
* Don't let the system encoding screw with things,
specify opens as happening with utf-8 encoding

* And also deal with encoding issues because windows is a special snowflake
(see: #1366)

* let's just use the encoding param in str() rather than encode/decode...
2018-03-04 13:02:04 -05:00
aikaterna
3816385228 [V3 Audio] Fix repeat, Message deletions (#1379)
* Fix repeat

Also remove restriction on if the player is playing.

* Auto-delete existing Now Playing message

* Auto-delete existing notify message

* Wrap line on Now Playing message

* Add connected duration to audiostats

_dynamic_time based on a function written by Redjumpman

* Return negative seek past song start as 00:00:00

* Version number
2018-03-03 18:55:24 -05:00
Will
f65085946c [V3 Audio] Initial V3 addition (#1373)
* Initial audio

* Add application data and modify port

* Modify codeowners

* Need extra newline

* add yml to manifest

* lock lavalink version
2018-03-03 15:42:20 +11:00
Thomas Mercurio
b10b746d9e [V3 Config] Correct Mongo connection URI without credentials (#1362)
* Correct connection string without credentials

* Remove extra whitespace.
2018-03-02 18:39:45 +01:00
James
ed5945e182 fixed exception (#1372)
`requirements.remove` will raise a ValueError if the item is not there, not `IndexError`
2018-03-02 18:35:28 +01:00
Will
cf48a13fc7 Potentially fix auto deploy issues (#1357) 2018-02-28 13:08:47 +01:00
palmtree5
29d9bbad32 [V3] Bump version to 3.0.0b9 (#1348) 2018-02-26 11:49:48 -09:00
Will
004f42bf6e [V3 Config] Limit connections to Mongo DB (#1356) 2018-02-26 15:34:30 -05:00
Will
41e0d105f6 [V3 Config] Make Mongo work with any db (#1355) 2018-02-26 15:28:20 -05:00
Will
fdd61963e6 Add pip install command (#1354) 2018-02-26 11:21:41 -09:00
Will
10cbd4d3c4 [V3 Sentry] Only log errors that come from core code (#1353)
* Do not load core cogs as extensions

* Filter this shit
2018-02-26 15:16:49 -05:00
Will
64af7800dc [V3 Config] Adjust functionality of get_attr (#1342)
* Intermediate commit

* Add defaulting stuff to config

* Remove set_attr in favor of set_raw

* Modify get_attr

* Fix issue with clearing data
2018-02-26 11:13:01 -09:00
palmtree5
c428982c00 [V3 Core] Add listlocales command (#1351)
* [V3] Add listlocales command + remove a hanging 2 letter translation

* Sort the locales list

* Doc info in set locale
2018-02-26 15:05:06 -05:00
Will
1363bc3f43 [V3] Disregard limitations on repo names (#1349) 2018-02-25 21:21:13 -09:00
Michael H
09ddfe4250 [V3] Re-allow using presence modifying commands without being in a guild (#1345)
Actually update on all shards (we need to do this even with autoshardedbot as base)
2018-02-25 21:10:59 -09:00
Will
e13de0950c [V3 Config] Implement a true clear method (#1344)
* Add clear methods to drivers

* Update config clear methods, add to Value

* Add test
2018-02-25 21:07:28 -09:00
palmtree5
c1ac78eea4 [V3] Warning system (#1173)
* [V3 Warning] initial work on a warning system for v3

* [V3 Warning] rename some stuff and add a case type

* [V3 Warnings] rename package from warning

* [V3 Warnings] restructuring commands

* [V3 Warnings] remove expiry stuff + other refactoring

* [V3 Warnings] refactoring action logic

* [V3 Warnings] rewrites to action logic

* [V3 Warnings] add regen_messages.py
2018-02-25 20:14:38 -09:00
Kowlin
21de95e0a6 [CI] Added support for conditional builds and env vars (#1320)
* Changed travis deploy scheme

* Fixed indentation

* Okay NOW I did fix indentation (hopefully)

* Conditional deployment tests

* Fixed a 7:35 AM typo

* Fixed builds not building.
2018-02-25 20:10:55 -09:00
palmtree5
0593bb9c53 [V3 i18n] Update translations from Crowdin (#1350) 2018-02-25 21:57:14 -05:00
Will
bd1ce26dc7 Fix missing repos folder (#1330) 2018-02-25 16:40:09 +01:00
Will
2612e7c23a [V3 Config] Add more information to the docs (#1343)
* Attempt to determine the registered default...if any

* Intermediate commit

* Add more doc information
2018-02-25 14:56:12 +01:00
Will
93ca7abc1a [V3 Core Commands] Limit status modifying commands to be run in guild only (#1331)
* Limit status modifying commands to be run in guild only

* JK let em run.

* Add a space
2018-02-23 02:44:06 +01:00
Will
de4af14676 Fix unreferenced tmp variable (#1334) 2018-02-23 02:29:04 +01:00
Will
c9a6c0ffcc Converters don't work the way I think they do (#1336)
(No they don't Will -- Kowlin)
2018-02-23 02:21:39 +01:00
Will
d414806fa2 Close aiohttp ClientSession (#1340) 2018-02-23 02:08:49 +01:00
Will
d54c129c57 [V3 Config] Allow users to directly specify a cog name with config (#1339) 2018-02-22 19:42:30 -05:00
Will
7c848153f8 [V3 Config] Limit config objects to a single one per cog (#1337)
* Limit config objects to a single one per cog

* do it correctly

* Do nicer thing

* do it right...

* I will get this right eventually
2018-02-22 19:36:07 -05:00
Will
4e36c2f82d [V3 Config] Add helper methods for advanced data access (#1335)
* Add get_raw/set_raw

* Add default to get_raw

* Add custom group method to the config object

* Add more methods for custom groups

* Add some docstrings

* Deepcopy the result of get_raw, throw TypeError if no value is passed to set_raw

* Do it right..

* Better way to require kwarg
2018-02-23 11:29:37 +11:00
Sebass13
b5c71bc59c [V3 Mod/Filter] Fix #1202 (#1327)
* Fix #1326

Checks to see if `author` is a `valid_user` (of `Member` class) before checking to see if the bot `is_mod_or_superior` to the `author`.

* Fix #1202

Raises `TypeError` when `User` objects are passed into `is_mod_or_superior` or `is_admin_or_superior`, as only `Member` objects have a `guild` attribute.

* Fixes #1202

Checks to see if `author` is a `valid_user` (of `Member` class) before checking to see if the bot `is_mod_or_superior` to the `author`.
2018-02-21 18:36:13 -05:00
Sebass13
d20724d7b2 [V3 General] Fix error in [p]userinfo when game is not set (#1325)
* Fix #1321

Modifies the game-checking lines so that they don't assume `user.game` is not a NoneType object.

* Fix #1321

Modifies the game-checking lines so that they don't assume user.game is not a NoneType object. Also alerts the user to go bother @tekulvw if somehow the invariants of `user` having a `game` attribute and the `game` attribute either being `None` of having a `type` attribute in `[0, 1, 2, 3]` are broken.

* Update general.py
2018-02-20 18:06:06 -05:00
Sebass13
1cca5ee5cb [V3 Downloader] Fixes issue #1323 (#1324)
The use of `popitem` makes the right side of line 642 return a tuple of the form `(repo, (old, new))` instead of a dictionary of the form `{repo: (old, new)}`, as expected by the left hand side of the same line.
2018-02-19 22:41:19 -05:00
palmtree5
95acf7402d Fix pip install in .travis.yml (#1319) 2018-02-18 18:56:09 -09:00
708 changed files with 106920 additions and 8072 deletions

15
.github/CODEOWNERS vendored
View File

@@ -21,11 +21,16 @@ redbot/core/rpc.py @tekulvw
redbot/core/sentry_setup.py @Kowlin @tekulvw
redbot/core/utils/chat_formatting.py @tekulvw
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
redbot/core/utils/common_filters.py @mikeshardmind
# Cogs
redbot/cogs/admin/* @tekulvw
redbot/cogs/alias/* @tekulvw
redbot/cogs/audio/* @tekulvw
redbot/cogs/audio/* @aikaterna @atiwiex
redbot/cogs/bank/* @tekulvw
redbot/cogs/cleanup/* @palmtree5
redbot/cogs/customcom/* @palmtree5
@@ -38,6 +43,10 @@ redbot/cogs/mod/* @palmtree5
redbot/cogs/modlog/* @palmtree5
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
@@ -47,3 +56,7 @@ setup.py @tekulvw
redbot/__init__.py @tekulvw
redbot/__main__.py @tekulvw
redbot/setup.py @tekulvw
# Others
.travis.yml @Kowlin
crowdin.yml @Kowlin

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.2 or greater (3.6.6 or greater on Windows)
- 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 and 3.7 (test environments `py36` and `py37`)
- 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 -N <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).

25
.github/ISSUE_TEMPLATE/command_bug.md vendored Normal file
View File

@@ -0,0 +1,25 @@
# Command bugs
<!--
Did you find a bug with a command? Fill out the following:
-->
#### Command name
<!-- Replace this line with the name of the command -->
#### What cog is this command from?
<!-- Replace this line with the name of the cog -->
#### What were you expecting to happen?
<!-- Replace this line with a description of what you were expecting to happen -->
#### What actually happened?
<!-- Replace this line with a description of what actually happened. Include any error messages -->
#### How can we reproduce this issue?
<!-- Replace with numbered steps to reproduce the issue -->

35
.github/ISSUE_TEMPLATE/feature_req.md vendored Normal file
View File

@@ -0,0 +1,35 @@
# Feature request
<!-- This template is for feature requests. Please fill out the following: -->
#### Select the type of feature you are requesting:
<!-- To check a box, replace the space between the [] with a x -->
- [ ] Cog
- [ ] Command
- [ ] API functionality
#### Describe your requested feature
<!--
Feel free to describe in as much detail as you wish.
If you are requesting a cog to be included in core:
- Describe the functionality in as much detail as possible
- Include the command structure, if possible
- Please note that unless it's something that should be core functionality,
we reserve the right to reject your suggestion and point you to our cog
board to request it for a third-party cog
If you are requesting a command:
- Include what cog it should be in and a name for the command
- Describe the intended functionality for the command
- Note any restrictions on who can use the command or where it can be used
If you are requesting API functionality:
- Describe what it should do
- Note whether it is to extend existing functionality or introduce new functionality
-->

21
.github/ISSUE_TEMPLATE/other_bug.md vendored Normal file
View File

@@ -0,0 +1,21 @@
# Other bugs
<!--
Did you find a bug with something other than a command? Fill out the following:
-->
#### What were you trying to do?
<!-- Replace this line with a description of what you were trying to do -->
#### What were you expecting to happen?
<!-- Replace this line with a description of what you were expecting to happen -->
#### What actually happened?
<!-- Replace this line with a description of what actually happened. Include any error messages -->
#### How can we reproduce this issue?
<!-- Replace with numbered steps to reproduce the issue -->

14
.github/PULL_REQUEST_TEMPLATE/bugfix.md vendored Normal file
View File

@@ -0,0 +1,14 @@
# Bugfix request
<!--
To be used for pull requests that fix a bug
-->
#### Describe the bug being fixed
<!--
If an issue exists for the bug, mention
that this PR fixes that issue
-->
#### Anything we need to know about this fix?

View File

@@ -0,0 +1,20 @@
# Enhancement request
<!--
To be used for PRs which enhance existing features
-->
#### Describe the enhancement
<!--
Describe what your changes do.
If adding commands, describe any restrictions on their usage.
- For example, who can use the command? Where can it be used?
-->
#### Does this enhancement break existing functionality?
<!-- To check a box, replace the space between the [] with a x -->
- [ ] Yes
- [ ] No

View File

@@ -0,0 +1,21 @@
# New feature addition
<!--
To be used for PRs which add a new feature
Examples of this include new APIs, new core cogs, etc.
-->
#### What type of feature is this?
<!-- To check a box, replace the space between the [] with a x -->
- [ ] New core cog
- [ ] New API
- [ ] Other
#### Describe the feature
<!--
If you are adding a cog, describe its commands in detail (functionality, usage restrictions, etc).
If the new feature introduces new requirements, please try to explain why they are necessary.
-->

View File

@@ -0,0 +1,16 @@
# New release
<!--
To be used by collaborators for doing releases.
Most contributors will not need to use this.
-->
#### Version
#### Has a draft release been created for this?
- [ ] Yes
- [ ] No

View File

@@ -0,0 +1,5 @@
# Translations update
<!--
Used for PRs updating translations from Crowdin
-->

32
.gitignore vendored
View File

@@ -1,40 +1,16 @@
# Trivia list repo injection
redbot/trivia/
*.json
*.exe
*.dll
*.pot
.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:
@@ -156,3 +132,5 @@ ENV/
# mypy
.mypy_cache/
# pytest
.pytest_cache/

12
.readthedocs.yml Normal file
View File

@@ -0,0 +1,12 @@
formats:
- pdf
build:
image: latest
python:
version: 3.6
pip_install: true
extra_requirements:
- docs
- mongo

View File

@@ -1,42 +1,67 @@
dist: trusty
dist: xenial
language: python
python:
- 3.5.3
- 3.6.1
install:
- echo "pytest>3" >> requirements.txt
- echo "pytest-asyncio" >> requirements.txt
- echo "git+https://github.com/Rapptz/discord.py.git@rewrite#egg=discord.py[voice]"
>> requirements.txt
- pip install -r requirements.txt
- pip install -e .
script:
- python -m compileall ./redbot/cogs
- python -m pytest
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
- sudo apt-get install -y crowdin
deploy:
- provider: pypi
user: Red-DiscordBot
password:
secure: Ty9vYnd/wCuQkVC/OsS4E2jT9LVDVfzsFrQc4U2hMYcTJnYbl/3omyObdCWCOBC40vUDkVHAQU8ULHzoCA+2KX9Ds/7/P5zCumAA0uJRR9Smw7OlRzSMxJI+/lGq4CwXKzxDZKuo5rsxXEbW5qmYjtO8Mk6KuLkvieb1vyr2DcqWEFzg/7TZNDfD1oP8et8ITQ26lLP1dtQx/jlAiIBzgK9wziuwj1Divb9A///VsGz43N8maZ+jfsDjYqrfUVWTy3ar7JPUplletenYCR1PmQ5C46XfV0kitKd1aITJ48YPAKyYgKy8AIT+Uz1JArTnqdzLSFRNELS57qS00lzgllbteCyWQ8Uzy0Zpxb/5DDH8/mL1n0MyJrF8qjZd2hLNAXg3z/k9bGXeiMLGwoxRlGXkL2XpiVgI93UKKyVyooGNMgPTc/QdSc7krjAWcOtX/HgLR34jxeLPFEdzJNAFIimfDD8N+XTFcNBw6EvOYm/n5MXkckNoX/G+ThNobHZ7VKSASltZ9zBRAJ2dDh35G3CYmVEk33U77RKbL9le/Za9QVBcAO8i6rqVGYkdO7thHHKHc/1CB1jNnjsFSDt0bURtNfAqfwKCurQC8487zbEzT+2fog3Wygv7g3cklaRg4guY8UjZuFWStYGqbroTsOCd9ATNqeO5B13pNhllSzU=
skip_cleanup: true
on:
repo: Cog-Creators/Red-DiscordBot
branch: V3/develop
python: 3.5.3
tags: true
- provider: script
script: python3 ./generate_strings.py
skip_cleanup: true
on:
repo: Cog-Creators/Red-DiscordBot
branch: V3/develop
python: 3.5.3
tags: true
cache: pip
notifications:
email: false
sudo: true
python:
- 3.6.6
- 3.7
env:
global:
PIPENV_IGNORE_VIRTUALENVS=1
matrix:
TOXENV=py
install:
- pip install --upgrade pip tox
script:
- tox
jobs:
include:
- python: 3.6.6
env: TOXENV=docs
- python: 3.6.6
env: TOXENV=style
# These jobs only occur on tag creation if the prior ones succeed
- stage: PyPi Deployment
if: tag IS present
python: 3.6.6
env:
- DEPLOYING=true
- TOXENV=py36
deploy:
- provider: pypi
user: Red-DiscordBot
password:
secure: Ty9vYnd/wCuQkVC/OsS4E2jT9LVDVfzsFrQc4U2hMYcTJnYbl/3omyObdCWCOBC40vUDkVHAQU8ULHzoCA+2KX9Ds/7/P5zCumAA0uJRR9Smw7OlRzSMxJI+/lGq4CwXKzxDZKuo5rsxXEbW5qmYjtO8Mk6KuLkvieb1vyr2DcqWEFzg/7TZNDfD1oP8et8ITQ26lLP1dtQx/jlAiIBzgK9wziuwj1Divb9A///VsGz43N8maZ+jfsDjYqrfUVWTy3ar7JPUplletenYCR1PmQ5C46XfV0kitKd1aITJ48YPAKyYgKy8AIT+Uz1JArTnqdzLSFRNELS57qS00lzgllbteCyWQ8Uzy0Zpxb/5DDH8/mL1n0MyJrF8qjZd2hLNAXg3z/k9bGXeiMLGwoxRlGXkL2XpiVgI93UKKyVyooGNMgPTc/QdSc7krjAWcOtX/HgLR34jxeLPFEdzJNAFIimfDD8N+XTFcNBw6EvOYm/n5MXkckNoX/G+ThNobHZ7VKSASltZ9zBRAJ2dDh35G3CYmVEk33U77RKbL9le/Za9QVBcAO8i6rqVGYkdO7thHHKHc/1CB1jNnjsFSDt0bURtNfAqfwKCurQC8487zbEzT+2fog3Wygv7g3cklaRg4guY8UjZuFWStYGqbroTsOCd9ATNqeO5B13pNhllSzU=
skip_cleanup: true
on:
repo: Cog-Creators/Red-DiscordBot
python: 3.6.6
tags: true
- stage: Crowdin Deployment
if: tag IS present
python: 3.6.6
env:
- DEPLOYING=true
- TOXENV=py36
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
- sudo apt-get install -y crowdin
- pip install redgettext==2.2
deploy:
- provider: script
script: make gettext
skip_cleanup: true
on:
repo: Cog-Creators/Red-DiscordBot
python: 3.6.6
tags: true

View File

@@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Red - A fully customizable Discord bot
Copyright (C) 2015-2018 Twentysix
Copyright (C) 2015-2019 Twentysix
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Red-DiscordBot Copyright (C) 2015-2018 Twentysix
Red-DiscordBot Copyright (C) 2015-2019 Twentysix
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.

View File

@@ -1,4 +0,0 @@
include README.rst
include LICENSE
include requirements.txt
include discord/bin/*.dll

13
Makefile Normal file
View File

@@ -0,0 +1,13 @@
reformat:
black -l 99 -N `git ls-files "*.py"`
stylecheck:
black --check -l 99 -N `git ls-files "*.py"`
gettext:
redgettext --command-docstrings --verbose --recursive redbot --exclude-files "redbot/pytest/**/*"
crowdin upload
REF?=rewrite
update_vendor:
pip install --upgrade --no-deps -t . https://github.com/Rapptz/discord.py/archive/$(REF).tar.gz#egg=discord.py
rm -r discord.py*-info
$(MAKE) reformat

11
Pipfile Normal file
View File

@@ -0,0 +1,11 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
red-discordbot = {path = ".",editable = true,extras = ['mongo', 'voice']}
[dev-packages]
tox = "*"
red-discordbot = {path = ".",editable = true,extras = ['docs', 'test', 'style']}

841
Pipfile.lock generated Normal file
View File

@@ -0,0 +1,841 @@
{
"_meta": {
"hash": {
"sha256": "b9f385e4c53c659dd76e8722d1fb69c244d3a76e4b0dfc40956ff2493277c1f6"
},
"pipfile-spec": 6,
"requires": {},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"aiohttp": {
"hashes": [
"sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55",
"sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed",
"sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10",
"sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5",
"sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1",
"sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939",
"sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390",
"sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa",
"sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc",
"sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5",
"sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d",
"sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf",
"sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6",
"sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72",
"sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12",
"sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366",
"sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4",
"sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300",
"sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d",
"sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303",
"sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6",
"sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"
],
"version": "==3.5.4"
},
"aiohttp-json-rpc": {
"hashes": [
"sha256:1d040b7b10ff414f9174398ff6e9c647eb0434a00939450b33aa539177c51dcf",
"sha256:5f5fb141c6263d2ea52a4173babe9449eef4029620dc49936dca45cdc17ac9dd"
],
"version": "==0.12"
},
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"async-timeout": {
"hashes": [
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
],
"version": "==3.0.1"
},
"attrs": {
"hashes": [
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
],
"version": "==18.2.0"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"colorama": {
"hashes": [
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
],
"version": "==0.4.1"
},
"distro": {
"hashes": [
"sha256:362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57",
"sha256:eedf82a470ebe7d010f1872c17237c79ab04097948800029994fa458e52fb4b4"
],
"version": "==1.4.0"
},
"dnspython": {
"hashes": [
"sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01",
"sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"
],
"version": "==1.16.0"
},
"fuzzywuzzy": {
"hashes": [
"sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254",
"sha256:6f49de47db00e1c71d40ad16da42284ac357936fa9b66bea1df63fed07122d62"
],
"version": "==0.17.0"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"idna-ssl": {
"hashes": [
"sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
],
"version": "==1.1.0"
},
"motor": {
"hashes": [
"sha256:462fbb824f4289481c158227a2579d6adaf1ec7c70cf7ebe60ed6ceb321e5869",
"sha256:d035c09ab422bc50bf3efb134f7405694cae76268545bd21e14fb22e2638f84e"
],
"version": "==2.0.0"
},
"multidict": {
"hashes": [
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3",
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef",
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b",
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73",
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc",
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3",
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd",
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351",
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941",
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d",
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1",
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b",
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a",
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3",
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7",
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0",
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0",
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014",
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5",
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036",
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d",
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a",
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce",
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1",
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a",
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9",
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7",
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"
],
"version": "==4.5.2"
},
"pymongo": {
"hashes": [
"sha256:025f94fc1e1364f00e50badc88c47f98af20012f23317234e51a11333ef986e6",
"sha256:02aa7fb282606331aefbc0586e2cf540e9dbe5e343493295e7f390936ad2738e",
"sha256:057210e831573e932702cf332012ed39da78edf0f02d24a3f0b213264a87a397",
"sha256:0d946b79c56187fe139276d4c8ed612a27a616966c8b9779d6b79e2053587c8b",
"sha256:104790893b928d310aae8a955e0bdbaa442fb0ac0a33d1bbb0741c791a407778",
"sha256:15527ef218d95a8717486106553b0d54ff2641e795b65668754e17ab9ca6e381",
"sha256:1826527a0b032f6e20e7ac7f72d7c26dd476a5e5aa82c04aa1c7088a59fded7d",
"sha256:22e3aa4ce1c3eebc7f70f9ca7fd4ce1ea33e8bdb7b61996806cd312f08f84a3a",
"sha256:244e1101e9a48615b9a16cbd194f73c115fdfefc96894803158608115f703b26",
"sha256:24b8c04fdb633a84829d03909752c385faef249c06114cc8d8e1700b95aae5c8",
"sha256:2c276696350785d3104412cbe3ac70ab1e3a10c408e7b20599ee41403a3ed630",
"sha256:2d8474dc833b1182b651b184ace997a7bd83de0f51244de988d3c30e49f07de3",
"sha256:3119b57fe1d964781e91a53e81532c85ed1701baaddec592e22f6b77a9fdf3df",
"sha256:3bee8e7e0709b0fcdaa498a3e513bde9ffc7cd09dbceb11e425bd91c89dbd5b6",
"sha256:436c071e01a464753d30dbfc8768dd93aecf2a8e378e5314d130b95e77b4d612",
"sha256:46635e3f19ad04d5a7d7cf23d232388ddbfccf46d9a3b7436b6abadda4e84813",
"sha256:4772e0b679717e7ac4608d996f57b6f380748a919b457cb05bb941467b888b22",
"sha256:4e2cd80e16f481a62c3175b607373200e714ed29025f21559ebf7524f295689f",
"sha256:52732960efa0e003ca1c092dc0a3c65276e897681287a788a01ca78dda3b41f0",
"sha256:55a7de51ec7d1731b2431886d0349146645f2816e5b8eb982d7c49f89472c9f3",
"sha256:5f8ed5934197a2d4b2087646e98de3e099a237099dcf498b9e38dd3465f74ef4",
"sha256:64b064124fcbc8eb04a155117dc4d9a336e3cda3f069958fbc44fe70c3c3d1e9",
"sha256:65958b8e4319f992e85dad59d8081888b97fcdbde5f0d14bc28f2848b92d3ef1",
"sha256:7683428862e20c6a790c19e64f8ccf487f613fbc83d47e3d532df9c81668d451",
"sha256:78566d5570c75a127c2491e343dc006798a384f06be588fe9b0cbe5595711559",
"sha256:7d1cb00c093dbf1d0b16ccf123e79dee3b82608e4a2a88947695f0460eef13ff",
"sha256:8c74e2a9b594f7962c62cef7680a4cb92a96b4e6e3c2f970790da67cc0213a7e",
"sha256:8e60aa7699170f55f4b0f56ee6f8415229777ac7e4b4b1aa41fc61eec08c1f1d",
"sha256:9447b561529576d89d3bf973e5241a88cf76e45bd101963f5236888713dea774",
"sha256:970055bfeb0be373f2f5299a3db8432444bad3bc2f198753ee6c2a3a781e0959",
"sha256:a6344b8542e584e140dc3c651d68bde51270e79490aa9320f9e708f9b2c39bd5",
"sha256:ce309ca470d747b02ba6069d286a17b7df8e9c94d10d727d9cf3a64e51d85184",
"sha256:cfbd86ed4c2b2ac71bbdbcea6669bf295def7152e3722ddd9dda94ac7981f33d",
"sha256:d7929c513732dff093481f4a0954ed5ff16816365842136b17caa0b4992e49d3"
],
"version": "==3.7.2"
},
"python-levenshtein-wheels": {
"hashes": [
"sha256:0065529c8aec4c044468286177761857d36981ba6f7fdb62d7d5f7ffd143de5d",
"sha256:016924a59d689f9f47d5f7b26b70f31e309255e8dd72602c91e93ceb752b9f92",
"sha256:089d046ea7727e583233c71fef1046663ed67b96967063ae8ddc9f551e86a4fc",
"sha256:0aea217eab612acd45dcc3424a2e8dbd977cc309f80359d0c01971f1e65b9a9b",
"sha256:0beb91ad80b1573829066e5af36b80190c367be6e0a65292f073353b0388c7fc",
"sha256:0fa2ca69ef803bc6037a8c919e2e8a17b55e94c9c9ffcb4c21befbb15a1d0f40",
"sha256:11c77d0d74ab7f46f89a58ae9c2d67349ebc1ae3e18636627f9939d810167c31",
"sha256:19a68716a322486ddffc8bf7e5cf44a82f7700b05a10658e6e7fc5c7ae92b13d",
"sha256:19a95a01d28d63b042438ba860c4ace90362906a038fa77962ba33325d377d10",
"sha256:1a61f3a51e00a3608659bbaabb3f27af37c9dbe84d843369061a3e45cf0d5103",
"sha256:1c50aebebab403fb2dd415d70355446ac364dece502b0e2737a1a085bb9a4aa4",
"sha256:1e51cdc123625a28709662d24ea0cb4cf6f991845e6054d9f803c78da1d6b08f",
"sha256:1f0056d3216b0fe38f25c6f8ebc84bd9f6d34c55a7a9414341b674fb98961399",
"sha256:228b59460e9a786e498bdfc8011838b89c6054650b115c86c9c819a055a793b0",
"sha256:23020f9ff2cb3457a926dcc470b84f9bd5b7646bd8b8e06b915bdbbc905cb23f",
"sha256:3e6bcca97a7ff4e720352b57ddc26380c0583dcdd4b791acef7b574ad58468a7",
"sha256:3ed88f9e638da57647149115c34e0e120cae6f3d35eee7d77e22cc9c1d8eced3",
"sha256:445bf7941cb1fa05d6c2a4a502ad4868a5cacd92e8eb77b2bd008cdda9d37c55",
"sha256:4ba5e147d76d7ee884fd6eae461438b080bcc9f2c6eb9b576811e1bcfe8f808e",
"sha256:4bb128b719c30f3b9feacfe71a338ae07d39dbffc077139416f3535c89f12362",
"sha256:53c0c9964390368fd64460b690f168221c669766b193b7e80ae3950c2b9551f8",
"sha256:57c4edef81611098d37176278f2b6a3712bf864eed313496d7d80504805896d1",
"sha256:7f7283dfe50eac8a8cd9b777de9eb50b1edf7dbb46fc7cc9d9b0050d0c135021",
"sha256:7f9759095b3fc825464a72b1cae95125e610eba3c70f91557754c32a0bf32ea2",
"sha256:98727050ba70eb8d318ec8a8203531c20119347fc8f281102b097326812742ab",
"sha256:ac9cdf044dcb9481c7da782db01b50c1f0e7cdd78c8507b963b6d072829c0263",
"sha256:b679f951f842c38665aa54bea4d7403099131f71fac6d8584f893a731fe1266d",
"sha256:b8c183dc4aa4e95dc5c373eedc3d205c176805835611fcfec5d9050736c695c4",
"sha256:c2c76f483d05eddec60a5cd89e92385adef565a4f243b1d9a6abe2f6bd2a7c0a",
"sha256:c388baa3c04272a7c585d3da24030c142353eb26eb531dd2681502e6be7d7a26",
"sha256:cb0f2a711db665b5bf8697b5af3b9884bb1139385c5c12c2e472e4bbee62da99",
"sha256:cbac984d7b36e75b440d1c8ff9d3425d778364a0cbc23f8943383d4decd35d5e",
"sha256:f9084ed3b8997ad4353d124b903f2860a9695b9e080663276d9e58c32e293244"
],
"version": "==0.13.1"
},
"pyyaml": {
"hashes": [
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
],
"version": "==3.13"
},
"raven": {
"hashes": [
"sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54",
"sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4"
],
"version": "==6.10.0"
},
"raven-aiohttp": {
"hashes": [
"sha256:1444a49c93a85b8bb57c6ee649e512368dce7a26ad64ac3a01d86aa5669d77f3",
"sha256:6a34b6a9841ad0fd827eeb158edb5826c5c5bd7babe2cde2a3f23eb85313af04"
],
"version": "==0.7.0"
},
"red-discordbot": {
"editable": true,
"extras": [
"mongo",
"voice"
],
"path": "."
},
"red-lavalink": {
"hashes": [
"sha256:13e1a3f91b990be9582cba039d9a32ec4cef760da1e7e6952143116ec83d4302",
"sha256:3dd0d73b4a908bbe9cfb703d2563dad1d1a58f8eea5896a0dacdf37d54a39d9c"
],
"version": "==0.2.3"
},
"schema": {
"hashes": [
"sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687",
"sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74"
],
"version": "==0.6.8"
},
"websockets": {
"hashes": [
"sha256:04b42a1b57096ffa5627d6a78ea1ff7fad3bc2c0331ffc17bc32a4024da7fea0",
"sha256:08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f",
"sha256:10d89d4326045bf5e15e83e9867c85d686b612822e4d8f149cf4840aab5f46e0",
"sha256:232fac8a1978fc1dead4b1c2fa27c7756750fb393eb4ac52f6bc87ba7242b2fa",
"sha256:4bf4c8097440eff22bc78ec76fe2a865a6e658b6977a504679aaf08f02c121da",
"sha256:51642ea3a00772d1e48fb0c492f0d3ae3b6474f34d20eca005a83f8c9c06c561",
"sha256:55d86102282a636e195dad68aaaf85b81d0bef449d7e2ef2ff79ac450bb25d53",
"sha256:564d2675682bd497b59907d2205031acbf7d3fadf8c763b689b9ede20300b215",
"sha256:5d13bf5197a92149dc0badcc2b699267ff65a867029f465accfca8abab95f412",
"sha256:5eda665f6789edb9b57b57a159b9c55482cbe5b046d7db458948370554b16439",
"sha256:5edb2524d4032be4564c65dc4f9d01e79fe8fad5f966e5b552f4e5164fef0885",
"sha256:79691794288bc51e2a3b8de2bc0272ca8355d0b8503077ea57c0716e840ebaef",
"sha256:7fcc8681e9981b9b511cdee7c580d5b005f3bb86b65bde2188e04a29f1d63317",
"sha256:8e447e05ec88b1b408a4c9cde85aa6f4b04f06aa874b9f0b8e8319faf51b1fee",
"sha256:90ea6b3e7787620bb295a4ae050d2811c807d65b1486749414f78cfd6fb61489",
"sha256:9e13239952694b8b831088431d15f771beace10edfcf9ef230cefea14f18508f",
"sha256:d40f081187f7b54d7a99d8a5c782eaa4edc335a057aa54c85059272ed826dc09",
"sha256:e1df1a58ed2468c7b7ce9a2f9752a32ad08eac2bcd56318625c3647c2cd2da6f",
"sha256:e98d0cec437097f09c7834a11c69d79fe6241729b23f656cfc227e93294fc242",
"sha256:f8d59627702d2ff27cb495ca1abdea8bd8d581de425c56e93bff6517134e0a9b",
"sha256:fc30cdf2e949a2225b012a7911d1d031df3d23e99b7eda7dfc982dc4a860dae9"
],
"version": "==7.0"
},
"yarl": {
"hashes": [
"sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9",
"sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f",
"sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb",
"sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320",
"sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842",
"sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0",
"sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829",
"sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310",
"sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4",
"sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8",
"sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"
],
"version": "==1.3.0"
}
},
"develop": {
"aiohttp": {
"hashes": [
"sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55",
"sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed",
"sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10",
"sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5",
"sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1",
"sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939",
"sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390",
"sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa",
"sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc",
"sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5",
"sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d",
"sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf",
"sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6",
"sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72",
"sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12",
"sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366",
"sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4",
"sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300",
"sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d",
"sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303",
"sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6",
"sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"
],
"version": "==3.5.4"
},
"aiohttp-json-rpc": {
"hashes": [
"sha256:1d040b7b10ff414f9174398ff6e9c647eb0434a00939450b33aa539177c51dcf",
"sha256:5f5fb141c6263d2ea52a4173babe9449eef4029620dc49936dca45cdc17ac9dd"
],
"version": "==0.12"
},
"alabaster": {
"hashes": [
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
],
"version": "==0.7.12"
},
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"async-timeout": {
"hashes": [
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
],
"version": "==3.0.1"
},
"atomicwrites": {
"hashes": [
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
],
"version": "==1.3.0"
},
"attrs": {
"hashes": [
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
],
"version": "==18.2.0"
},
"babel": {
"hashes": [
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
],
"version": "==2.6.0"
},
"black": {
"hashes": [
"sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739",
"sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"
],
"version": "==18.9b0"
},
"certifi": {
"hashes": [
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
],
"version": "==2018.11.29"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"colorama": {
"hashes": [
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
],
"version": "==0.4.1"
},
"distro": {
"hashes": [
"sha256:362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57",
"sha256:eedf82a470ebe7d010f1872c17237c79ab04097948800029994fa458e52fb4b4"
],
"version": "==1.4.0"
},
"docutils": {
"hashes": [
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
],
"version": "==0.14"
},
"filelock": {
"hashes": [
"sha256:b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633",
"sha256:d610c1bb404daf85976d7a82eb2ada120f04671007266b708606565dd03b5be6"
],
"version": "==3.0.10"
},
"fuzzywuzzy": {
"hashes": [
"sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254",
"sha256:6f49de47db00e1c71d40ad16da42284ac357936fa9b66bea1df63fed07122d62"
],
"version": "==0.17.0"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"idna-ssl": {
"hashes": [
"sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
],
"version": "==1.1.0"
},
"imagesize": {
"hashes": [
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
],
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"markupsafe": {
"hashes": [
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
],
"version": "==1.1.0"
},
"more-itertools": {
"hashes": [
"sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
"sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
],
"version": "==6.0.0"
},
"multidict": {
"hashes": [
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3",
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef",
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b",
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73",
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc",
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3",
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd",
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351",
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941",
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d",
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1",
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b",
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a",
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3",
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7",
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0",
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0",
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014",
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5",
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036",
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d",
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a",
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce",
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1",
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a",
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9",
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7",
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"
],
"version": "==4.5.2"
},
"packaging": {
"hashes": [
"sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af",
"sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"
],
"version": "==19.0"
},
"pluggy": {
"hashes": [
"sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616",
"sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"
],
"version": "==0.8.1"
},
"py": {
"hashes": [
"sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
"sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
],
"version": "==1.7.0"
},
"pygments": {
"hashes": [
"sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a",
"sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"
],
"version": "==2.3.1"
},
"pyparsing": {
"hashes": [
"sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a",
"sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3"
],
"version": "==2.3.1"
},
"pytest": {
"hashes": [
"sha256:65aeaa77ae87c7fc95de56285282546cfa9c886dc8e5dc78313db1c25e21bc07",
"sha256:6ac6d467d9f053e95aaacd79f831dbecfe730f419c6c7022cb316b365cd9199d"
],
"version": "==4.2.0"
},
"pytest-asyncio": {
"hashes": [
"sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf",
"sha256:d734718e25cfc32d2bf78d346e99d33724deeba774cc4afdf491530c6184b63b"
],
"version": "==0.10.0"
},
"python-levenshtein-wheels": {
"hashes": [
"sha256:0065529c8aec4c044468286177761857d36981ba6f7fdb62d7d5f7ffd143de5d",
"sha256:016924a59d689f9f47d5f7b26b70f31e309255e8dd72602c91e93ceb752b9f92",
"sha256:089d046ea7727e583233c71fef1046663ed67b96967063ae8ddc9f551e86a4fc",
"sha256:0aea217eab612acd45dcc3424a2e8dbd977cc309f80359d0c01971f1e65b9a9b",
"sha256:0beb91ad80b1573829066e5af36b80190c367be6e0a65292f073353b0388c7fc",
"sha256:0fa2ca69ef803bc6037a8c919e2e8a17b55e94c9c9ffcb4c21befbb15a1d0f40",
"sha256:11c77d0d74ab7f46f89a58ae9c2d67349ebc1ae3e18636627f9939d810167c31",
"sha256:19a68716a322486ddffc8bf7e5cf44a82f7700b05a10658e6e7fc5c7ae92b13d",
"sha256:19a95a01d28d63b042438ba860c4ace90362906a038fa77962ba33325d377d10",
"sha256:1a61f3a51e00a3608659bbaabb3f27af37c9dbe84d843369061a3e45cf0d5103",
"sha256:1c50aebebab403fb2dd415d70355446ac364dece502b0e2737a1a085bb9a4aa4",
"sha256:1e51cdc123625a28709662d24ea0cb4cf6f991845e6054d9f803c78da1d6b08f",
"sha256:1f0056d3216b0fe38f25c6f8ebc84bd9f6d34c55a7a9414341b674fb98961399",
"sha256:228b59460e9a786e498bdfc8011838b89c6054650b115c86c9c819a055a793b0",
"sha256:23020f9ff2cb3457a926dcc470b84f9bd5b7646bd8b8e06b915bdbbc905cb23f",
"sha256:3e6bcca97a7ff4e720352b57ddc26380c0583dcdd4b791acef7b574ad58468a7",
"sha256:3ed88f9e638da57647149115c34e0e120cae6f3d35eee7d77e22cc9c1d8eced3",
"sha256:445bf7941cb1fa05d6c2a4a502ad4868a5cacd92e8eb77b2bd008cdda9d37c55",
"sha256:4ba5e147d76d7ee884fd6eae461438b080bcc9f2c6eb9b576811e1bcfe8f808e",
"sha256:4bb128b719c30f3b9feacfe71a338ae07d39dbffc077139416f3535c89f12362",
"sha256:53c0c9964390368fd64460b690f168221c669766b193b7e80ae3950c2b9551f8",
"sha256:57c4edef81611098d37176278f2b6a3712bf864eed313496d7d80504805896d1",
"sha256:7f7283dfe50eac8a8cd9b777de9eb50b1edf7dbb46fc7cc9d9b0050d0c135021",
"sha256:7f9759095b3fc825464a72b1cae95125e610eba3c70f91557754c32a0bf32ea2",
"sha256:98727050ba70eb8d318ec8a8203531c20119347fc8f281102b097326812742ab",
"sha256:ac9cdf044dcb9481c7da782db01b50c1f0e7cdd78c8507b963b6d072829c0263",
"sha256:b679f951f842c38665aa54bea4d7403099131f71fac6d8584f893a731fe1266d",
"sha256:b8c183dc4aa4e95dc5c373eedc3d205c176805835611fcfec5d9050736c695c4",
"sha256:c2c76f483d05eddec60a5cd89e92385adef565a4f243b1d9a6abe2f6bd2a7c0a",
"sha256:c388baa3c04272a7c585d3da24030c142353eb26eb531dd2681502e6be7d7a26",
"sha256:cb0f2a711db665b5bf8697b5af3b9884bb1139385c5c12c2e472e4bbee62da99",
"sha256:cbac984d7b36e75b440d1c8ff9d3425d778364a0cbc23f8943383d4decd35d5e",
"sha256:f9084ed3b8997ad4353d124b903f2860a9695b9e080663276d9e58c32e293244"
],
"version": "==0.13.1"
},
"pytz": {
"hashes": [
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
],
"version": "==2018.9"
},
"pyyaml": {
"hashes": [
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
],
"version": "==3.13"
},
"raven": {
"hashes": [
"sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54",
"sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4"
],
"version": "==6.10.0"
},
"raven-aiohttp": {
"hashes": [
"sha256:1444a49c93a85b8bb57c6ee649e512368dce7a26ad64ac3a01d86aa5669d77f3",
"sha256:6a34b6a9841ad0fd827eeb158edb5826c5c5bd7babe2cde2a3f23eb85313af04"
],
"version": "==0.7.0"
},
"red-discordbot": {
"editable": true,
"extras": [
"mongo",
"voice"
],
"path": "."
},
"red-lavalink": {
"hashes": [
"sha256:13e1a3f91b990be9582cba039d9a32ec4cef760da1e7e6952143116ec83d4302",
"sha256:3dd0d73b4a908bbe9cfb703d2563dad1d1a58f8eea5896a0dacdf37d54a39d9c"
],
"version": "==0.2.3"
},
"requests": {
"hashes": [
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
],
"version": "==2.21.0"
},
"schema": {
"hashes": [
"sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687",
"sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74"
],
"version": "==0.6.8"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.12.0"
},
"snowballstemmer": {
"hashes": [
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
],
"version": "==1.2.1"
},
"sphinx": {
"hashes": [
"sha256:b53904fa7cb4b06a39409a492b949193a1b68cc7241a1a8ce9974f86f0d24287",
"sha256:c1c00fc4f6e8b101a0d037065043460dffc2d507257f2f11acaed71fd2b0c83c"
],
"version": "==1.8.4"
},
"sphinx-rtd-theme": {
"hashes": [
"sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4",
"sha256:728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a"
],
"version": "==0.4.3"
},
"sphinxcontrib-asyncio": {
"hashes": [
"sha256:96627b1ec4eba08d09ad577ff9416c131910333ef37a2c82a2716e59646739f0"
],
"version": "==0.2.0"
},
"sphinxcontrib-websupport": {
"hashes": [
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
],
"version": "==1.1.0"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
},
"tox": {
"hashes": [
"sha256:04f8f1aa05de8e76d7a266ccd14e0d665d429977cd42123bc38efa9b59964e9e",
"sha256:25ef928babe88c71e3ed3af0c464d1160b01fca2dd1870a5bb26c2dea61a17fc"
],
"index": "pypi",
"version": "==3.7.0"
},
"urllib3": {
"hashes": [
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
],
"version": "==1.24.1"
},
"virtualenv": {
"hashes": [
"sha256:8b9abfc51c38b70f61634bf265e5beacf6fae11fc25d355d1871f49b8e45f0db",
"sha256:cceab52aa7d4df1e1871a70236eb2b89fcfe29b6b43510d9738689787c513261"
],
"version": "==16.4.0"
},
"websockets": {
"hashes": [
"sha256:04b42a1b57096ffa5627d6a78ea1ff7fad3bc2c0331ffc17bc32a4024da7fea0",
"sha256:08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f",
"sha256:10d89d4326045bf5e15e83e9867c85d686b612822e4d8f149cf4840aab5f46e0",
"sha256:232fac8a1978fc1dead4b1c2fa27c7756750fb393eb4ac52f6bc87ba7242b2fa",
"sha256:4bf4c8097440eff22bc78ec76fe2a865a6e658b6977a504679aaf08f02c121da",
"sha256:51642ea3a00772d1e48fb0c492f0d3ae3b6474f34d20eca005a83f8c9c06c561",
"sha256:55d86102282a636e195dad68aaaf85b81d0bef449d7e2ef2ff79ac450bb25d53",
"sha256:564d2675682bd497b59907d2205031acbf7d3fadf8c763b689b9ede20300b215",
"sha256:5d13bf5197a92149dc0badcc2b699267ff65a867029f465accfca8abab95f412",
"sha256:5eda665f6789edb9b57b57a159b9c55482cbe5b046d7db458948370554b16439",
"sha256:5edb2524d4032be4564c65dc4f9d01e79fe8fad5f966e5b552f4e5164fef0885",
"sha256:79691794288bc51e2a3b8de2bc0272ca8355d0b8503077ea57c0716e840ebaef",
"sha256:7fcc8681e9981b9b511cdee7c580d5b005f3bb86b65bde2188e04a29f1d63317",
"sha256:8e447e05ec88b1b408a4c9cde85aa6f4b04f06aa874b9f0b8e8319faf51b1fee",
"sha256:90ea6b3e7787620bb295a4ae050d2811c807d65b1486749414f78cfd6fb61489",
"sha256:9e13239952694b8b831088431d15f771beace10edfcf9ef230cefea14f18508f",
"sha256:d40f081187f7b54d7a99d8a5c782eaa4edc335a057aa54c85059272ed826dc09",
"sha256:e1df1a58ed2468c7b7ce9a2f9752a32ad08eac2bcd56318625c3647c2cd2da6f",
"sha256:e98d0cec437097f09c7834a11c69d79fe6241729b23f656cfc227e93294fc242",
"sha256:f8d59627702d2ff27cb495ca1abdea8bd8d581de425c56e93bff6517134e0a9b",
"sha256:fc30cdf2e949a2225b012a7911d1d031df3d23e99b7eda7dfc982dc4a860dae9"
],
"version": "==7.0"
},
"yarl": {
"hashes": [
"sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9",
"sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f",
"sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb",
"sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320",
"sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842",
"sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0",
"sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829",
"sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310",
"sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4",
"sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8",
"sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"
],
"version": "==1.3.0"
}
}
}

140
README.md Normal file
View File

@@ -0,0 +1,140 @@
<h1 align="center">
<br>
<a href="https://github.com/Cog-Creators/Red-DiscordBot/tree/V3/develop"><img src="https://imgur.com/pY1WUFX.png" alt="Red - Discord Bot"></a>
<br>
Red Discord Bot
<br>
</h1>
<h4 align="center">Music, Moderation, Trivia, Stream Alerts and Fully Modular.</h4>
<p align="center">
<a href="https://discord.gg/red">
<img src="https://discordapp.com/api/guilds/133049272517001216/widget.png?style=shield" alt="Discord Server">
</a>
<a href="https://www.patreon.com/Red_Devs">
<img src="https://img.shields.io/badge/Support-Red!-yellow.svg" alt="Support Red on Patreon!">
</a>
<a href="https://www.python.org/downloads/">
<img src="https://img.shields.io/badge/Made%20With-Python%203-blue.svg?style=for-the-badge" alt="Made with Python 3">
</a>
<a href="https://crowdin.com/project/red-discordbot">
<img src="https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg" alt="Localized with Crowdin">
</a>
<a href="https://github.com/Rapptz/discord.py/tree/rewrite">
<img src="https://img.shields.io/badge/discord-py-blue.svg" alt="discord.py">
</a>
</p>
<p align="center">
<a href="https://travis-ci.org/Cog-Creators/Red-DiscordBot">
<img src="https://api.travis-ci.org/Cog-Creators/Red-DiscordBot.svg?branch=V3/develop" alt="Travis CI">
</a>
<a href="http://red-discordbot.readthedocs.io/en/v3-develop/?badge=v3-develop">
<img src="https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop" alt="Red on readthedocs.org">
</a>
<a href="https://github.com/ambv/black">
<img src="https://img.shields.io/badge/code%20style-black-000000.svg" alt="Code Style: Black">
</a>
<a href="http://makeapullrequest.com">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg">
</a>
</p>
<p align="center">
<a href="#overview">Overview</a>
<a href="#installation">Installation</a>
<a href="http://red-discordbot.readthedocs.io/en/v3-develop/index.html">Documentation</a>
<a href="#plugins">Plugins</a>
<a href="#join-the-community">Community</a>
<a href="#license">License</a>
</p>
# Overview
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!
[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.
**The default set of modules includes and is not limited to:**
- 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)
- Bank (slot machine, user credits)
- Custom commands
- Imgur/gif search
- Admin automation (self-role assignment, cross-server announcements, mod-mail reports)
- Customisable command permissions
**Additionally, other [plugins](#plugins) (cogs) can be easily found and added from our growing
community of cog repositories.**
# 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_linux_mac.html)
- [Ubuntu](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
- [Debian Stretch](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
- [CentOS 7](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
- [Arch Linux](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
- [Raspbian Stretch](https://red-discordbot.readthedocs.io/en/v3-develop/install_linux_mac.html)
Already using **Red** V2? Take a look at the [Data Converter](https://red-discordbot.readthedocs.io/en/v3-develop/cog_dataconverter.html)
to import your data to V3.
If after reading the guide you are still experiencing issues, feel free to join the
[Official Discord Server](https://discord.gg/red) and ask in the **#v3-support** channel for help.
# Plugins
Red is fully modular, allowing you to load and unload plugins of your choice, and install 3rd party
plugins directly from Discord! A few examples are:
- Cleverbot integration (talk to Red and she talks back)
- Ban sync
- Welcome messages
- Casino
- Reaction roles
- Slow Mode
- Anilist
- And much, much more!
Feel free to take a [peek](https://github.com/Cog-Creators/Red-DiscordBot/issues/1398) at a list of
available 3rd party cogs!
# 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) the cog 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](https://www.gnu.org/licenses/gpl-3.0.en.html) license.
This project vendors the
[discord.py library by Rapptz](https://github.com/Rapptz/discord.py/tree/rewrite), which is
licensed under the [MIT License](https://opensource.org/licenses/MIT). This amounts to everything
within the *discord* folder of this repository.
Red is named after the main character of "Transistor", a video game by
[Super Giant Games](https://www.supergiantgames.com/games/transistor/).
Artwork created by [Sinlaire](https://sinlaire.deviantart.com/) on Deviant Art for the Red Discord
Bot Project.

View File

@@ -1,38 +0,0 @@
.. 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/PRs-welcome-brightgreen.svg?style=flat-square
:target: http://makeapullrequest.com
:alt: PRs Welcome
.. image:: https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg
:target: https://crowdin.com/project/red-discordbot
:alt: Crowdin
********************
Red - Discord Bot v3
********************
**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.**
How to install
^^^^^^^^^^^^^^
Using python3 pip::
pip install --process-dependency-links -U Red-DiscordBot
redbot-setup
redbot <name>
To install requirements for voice::
pip install --process-dependency-links -U Red-DiscordBot[voice]
To install all requirements for docs and tests::
pip install --process-dependency-links -U Red-DiscordBot[test,docs]
For the latest git build, replace ``Red-DiscordBot`` in the above commands with
``git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop``.

View File

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

64
discord/__init__.py Normal file
View File

@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
"""
Discord API Wrapper
~~~~~~~~~~~~~~~~~~~
A basic wrapper for the Discord API.
:copyright: (c) 2015-2019 Rapptz
:license: MIT, see LICENSE for more details.
"""
__title__ = "discord"
__author__ = "Rapptz"
__license__ = "MIT"
__copyright__ = "Copyright 2015-2019 Rapptz"
__version__ = "1.0.0a"
from collections import namedtuple
import logging
from .client import Client, AppInfo
from .user import User, ClientUser, Profile
from .emoji import Emoji, PartialEmoji
from .activity import *
from .channel import *
from .guild import Guild
from .relationship import Relationship
from .member import Member, VoiceState
from .message import Message, Attachment
from .errors import *
from .calls import CallMessage, GroupCall
from .permissions import Permissions, PermissionOverwrite
from .role import Role
from .file import File
from .colour import Color, Colour
from .invite import Invite
from .object import Object
from .reaction import Reaction
from . import utils, opus, abc
from .enums import *
from .embeds import Embed
from .shard import AutoShardedClient
from .player import *
from .webhook import *
from .voice_client import VoiceClient
from .audit_logs import AuditLogChanges, AuditLogEntry, AuditLogDiff
from .raw_models import *
VersionInfo = namedtuple("VersionInfo", "major minor micro releaselevel serial")
version_info = VersionInfo(major=1, minor=0, micro=0, releaselevel="alpha", serial=0)
try:
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass
logging.getLogger(__name__).addHandler(NullHandler())

337
discord/__main__.py Normal file
View File

@@ -0,0 +1,337 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import argparse
import sys
from pathlib import Path
import discord
def core(parser, args):
pass
bot_template = """#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from discord.ext import commands
import discord
import config
class Bot(commands.{base}):
def __init__(self, **kwargs):
super().__init__(command_prefix=commands.when_mentioned_or('{prefix}'), **kwargs)
for cog in config.cogs:
try:
self.load_extension(cog)
except Exception as exc:
print('Could not load extension {{0}} due to {{1.__class__.__name__}}: {{1}}'.format(cog, exc))
async def on_ready(self):
print('Logged on as {{0}} (ID: {{0.id}})'.format(self.user))
bot = Bot()
# write general commands here
bot.run(config.token)
"""
gitignore_template = """# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Our configuration files
config.py
"""
cog_template = '''# -*- coding: utf-8 -*-
from discord.ext import commands
import discord
class {name}:
"""The description for {name} goes here."""
def __init__(self, bot):
self.bot = bot
{extra}
def setup(bot):
bot.add_cog({name}(bot))
'''
cog_extras = """
def __unload(self):
# clean up logic goes here
pass
async def __local_check(self, ctx):
# checks that apply to every command in here
return True
async def __global_check(self, ctx):
# checks that apply to every command to the bot
return True
async def __global_check_once(self, ctx):
# check that apply to every command but is guaranteed to be called only once
return True
async def __error(self, ctx, error):
# error handling to every command in here
pass
async def __before_invoke(self, ctx):
# called before a command is called here
pass
async def __after_invoke(self, ctx):
# called after a command is called here
pass
"""
# certain file names and directory names are forbidden
# see: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
# although some of this doesn't apply to Linux, we might as well be consistent
_base_table = {
"<": "-",
">": "-",
":": "-",
'"': "-",
# '/': '-', these are fine
# '\\': '-',
"|": "-",
"?": "-",
"*": "-",
}
#
_base_table.update((chr(i), None) for i in range(32))
translation_table = str.maketrans(_base_table)
def to_path(parser, name, *, replace_spaces=False):
if isinstance(name, Path):
return name
if sys.platform == "win32":
forbidden = (
"CON",
"PRN",
"AUX",
"NUL",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9",
)
if len(name) <= 4 and name.upper() in forbidden:
parser.error("invalid directory name given, use a different one")
name = name.translate(translation_table)
if replace_spaces:
name = name.replace(" ", "-")
return Path(name)
def newbot(parser, args):
if sys.version_info < (3, 5):
parser.error("python version is older than 3.5, consider upgrading.")
new_directory = to_path(parser, args.directory) / to_path(parser, args.name)
# as a note exist_ok for Path is a 3.5+ only feature
# since we already checked above that we're >3.5
try:
new_directory.mkdir(exist_ok=True, parents=True)
except OSError as exc:
parser.error("could not create our bot directory ({})".format(exc))
cogs = new_directory / "cogs"
try:
cogs.mkdir(exist_ok=True)
init = cogs / "__init__.py"
init.touch()
except OSError as exc:
print("warning: could not create cogs directory ({})".format(exc))
try:
with open(str(new_directory / "config.py"), "w", encoding="utf-8") as fp:
fp.write('token = "place your token here"\ncogs = []\n')
except OSError as exc:
parser.error("could not create config file ({})".format(exc))
try:
with open(str(new_directory / "bot.py"), "w", encoding="utf-8") as fp:
base = "Bot" if not args.sharded else "AutoShardedBot"
fp.write(bot_template.format(base=base, prefix=args.prefix))
except OSError as exc:
parser.error("could not create bot file ({})".format(exc))
if not args.no_git:
try:
with open(str(new_directory / ".gitignore"), "w", encoding="utf-8") as fp:
fp.write(gitignore_template)
except OSError as exc:
print("warning: could not create .gitignore file ({})".format(exc))
print("successfully made bot at", new_directory)
def newcog(parser, args):
if sys.version_info < (3, 5):
parser.error("python version is older than 3.5, consider upgrading.")
cog_dir = to_path(parser, args.directory)
try:
cog_dir.mkdir(exist_ok=True)
except OSError as exc:
print("warning: could not create cogs directory ({})".format(exc))
directory = cog_dir / to_path(parser, args.name)
directory = directory.with_suffix(".py")
try:
with open(str(directory), "w", encoding="utf-8") as fp:
extra = cog_extras if args.full else ""
if args.class_name:
name = args.class_name
else:
name = str(directory.stem)
if "-" in name:
name = name.replace("-", " ").title().replace(" ", "")
else:
name = name.title()
fp.write(cog_template.format(name=name, extra=extra))
except OSError as exc:
parser.error("could not create cog file ({})".format(exc))
else:
print("successfully made cog at", directory)
def add_newbot_args(subparser):
parser = subparser.add_parser("newbot", help="creates a command bot project quickly")
parser.set_defaults(func=newbot)
parser.add_argument("name", help="the bot project name")
parser.add_argument(
"directory",
help="the directory to place it in (default: .)",
nargs="?",
default=Path.cwd(),
)
parser.add_argument(
"--prefix", help="the bot prefix (default: $)", default="$", metavar="<prefix>"
)
parser.add_argument("--sharded", help="whether to use AutoShardedBot", action="store_true")
parser.add_argument(
"--no-git", help="do not create a .gitignore file", action="store_true", dest="no_git"
)
def add_newcog_args(subparser):
parser = subparser.add_parser("newcog", help="creates a new cog template quickly")
parser.set_defaults(func=newcog)
parser.add_argument("name", help="the cog name")
parser.add_argument(
"directory",
help="the directory to place it in (default: cogs)",
nargs="?",
default=Path("cogs"),
)
parser.add_argument(
"--class-name", help="the class name of the cog (default: <name>)", dest="class_name"
)
parser.add_argument("--full", help="add all special methods as well", action="store_true")
def parse_args():
parser = argparse.ArgumentParser(
prog="discord", description="Tools for helping with discord.py"
)
version = "discord.py v{0.__version__} for Python {1[0]}.{1[1]}.{1[2]}".format(
discord, sys.version_info
)
parser.add_argument(
"-v", "--version", action="version", version=version, help="shows the library version"
)
parser.set_defaults(func=core)
subparser = parser.add_subparsers(dest="subcommand", title="subcommands")
add_newbot_args(subparser)
add_newcog_args(subparser)
return parser, parser.parse_args()
def main():
parser, args = parse_args()
args.func(parser, args)
main()

1030
discord/abc.py Normal file

File diff suppressed because it is too large Load Diff

613
discord/activity.py Normal file
View File

@@ -0,0 +1,613 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import datetime
from .enums import ActivityType, try_enum
from .colour import Colour
__all__ = ["Activity", "Streaming", "Game", "Spotify"]
"""If curious, this is the current schema for an activity.
It's fairly long so I will document it here:
All keys are optional.
state: str (max: 128),
details: str (max: 128)
timestamps: dict
start: int (min: 1)
end: int (min: 1)
assets: dict
large_image: str (max: 32)
large_text: str (max: 128)
small_image: str (max: 32)
small_text: str (max: 128)
party: dict
id: str (max: 128),
size: List[int] (max-length: 2)
elem: int (min: 1)
secrets: dict
match: str (max: 128)
join: str (max: 128)
spectate: str (max: 128)
instance: bool
application_id: str
name: str (max: 128)
url: str
type: int
sync_id: str
session_id: str
flags: int
There are also activity flags which are mostly uninteresting for the library atm.
t.ActivityFlags = {
INSTANCE: 1,
JOIN: 2,
SPECTATE: 4,
JOIN_REQUEST: 8,
SYNC: 16,
PLAY: 32
}
"""
class _ActivityTag:
__slots__ = ()
class Activity(_ActivityTag):
"""Represents an activity in Discord.
This could be an activity such as streaming, playing, listening
or watching.
For memory optimisation purposes, some activities are offered in slimmed
down versions:
- :class:`Game`
- :class:`Streaming`
Attributes
------------
application_id: :class:`str`
The application ID of the game.
name: :class:`str`
The name of the activity.
url: :class:`str`
A stream URL that the activity could be doing.
type: :class:`ActivityType`
The type of activity currently being done.
state: :class:`str`
The user's current state. For example, "In Game".
details: :class:`str`
The detail of the user's current activity.
timestamps: :class:`dict`
A dictionary of timestamps. It contains the following optional keys:
- ``start``: Corresponds to when the user started doing the
activity in milliseconds since Unix epoch.
- ``end``: Corresponds to when the user will finish doing the
activity in milliseconds since Unix epoch.
assets: :class:`dict`
A dictionary representing the images and their hover text of an activity.
It contains the following optional keys:
- ``large_image``: A string representing the ID for the large image asset.
- ``large_text``: A string representing the text when hovering over the large image asset.
- ``small_image``: A string representing the ID for the small image asset.
- ``small_text``: A string representing the text when hovering over the small image asset.
party: :class:`dict`
A dictionary representing the activity party. It contains the following optional keys:
- ``id``: A string representing the party ID.
- ``size``: A list of up to two integer elements denoting (current_size, maximum_size).
"""
__slots__ = (
"state",
"details",
"timestamps",
"assets",
"party",
"flags",
"sync_id",
"session_id",
"type",
"name",
"url",
"application_id",
)
def __init__(self, **kwargs):
self.state = kwargs.pop("state", None)
self.details = kwargs.pop("details", None)
self.timestamps = kwargs.pop("timestamps", {})
self.assets = kwargs.pop("assets", {})
self.party = kwargs.pop("party", {})
self.application_id = kwargs.pop("application_id", None)
self.name = kwargs.pop("name", None)
self.url = kwargs.pop("url", None)
self.flags = kwargs.pop("flags", 0)
self.sync_id = kwargs.pop("sync_id", None)
self.session_id = kwargs.pop("session_id", None)
self.type = try_enum(ActivityType, kwargs.pop("type", -1))
def to_dict(self):
ret = {}
for attr in self.__slots__:
value = getattr(self, attr, None)
if value is None:
continue
if isinstance(value, dict) and len(value) == 0:
continue
ret[attr] = value
ret["type"] = int(self.type)
return ret
@property
def start(self):
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable."""
try:
return datetime.datetime.utcfromtimestamp(self.timestamps["start"] / 1000)
except KeyError:
return None
@property
def end(self):
"""Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable."""
try:
return datetime.datetime.utcfromtimestamp(self.timestamps["end"] / 1000)
except KeyError:
return None
@property
def large_image_url(self):
"""Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity if applicable."""
if self.application_id is None:
return None
try:
large_image = self.assets["large_image"]
except KeyError:
return None
else:
return "https://cdn.discordapp.com/app-assets/{0}/{1}.png".format(
self.application_id, large_image
)
@property
def small_image_url(self):
"""Optional[:class:`str`]: Returns a URL pointing to the small image asset of this activity if applicable."""
if self.application_id is None:
return None
try:
small_image = self.assets["small_image"]
except KeyError:
return None
else:
return "https://cdn.discordapp.com/app-assets/{0}/{1}.png".format(
self.application_id, small_image
)
@property
def large_image_text(self):
"""Optional[:class:`str`]: Returns the large image asset hover text of this activity if applicable."""
return self.assets.get("large_text", None)
@property
def small_image_text(self):
"""Optional[:class:`str`]: Returns the small image asset hover text of this activity if applicable."""
return self.assets.get("small_text", None)
class Game(_ActivityTag):
"""A slimmed down version of :class:`Activity` that represents a Discord game.
This is typically displayed via **Playing** on the official Discord client.
.. container:: operations
.. describe:: x == y
Checks if two games are equal.
.. describe:: x != y
Checks if two games are not equal.
.. describe:: hash(x)
Returns the game's hash.
.. describe:: str(x)
Returns the game's name.
Parameters
-----------
name: :class:`str`
The game's name.
start: Optional[:class:`datetime.datetime`]
A naive UTC timestamp representing when the game started. Keyword-only parameter. Ignored for bots.
end: Optional[:class:`datetime.datetime`]
A naive UTC timestamp representing when the game ends. Keyword-only parameter. Ignored for bots.
Attributes
-----------
name: :class:`str`
The game's name.
"""
__slots__ = ("name", "_end", "_start")
def __init__(self, name, **extra):
self.name = name
try:
timestamps = extra["timestamps"]
except KeyError:
self._extract_timestamp(extra, "start")
self._extract_timestamp(extra, "end")
else:
self._start = timestamps.get("start", 0)
self._end = timestamps.get("end", 0)
def _extract_timestamp(self, data, key):
try:
dt = data[key]
except KeyError:
setattr(self, "_" + key, 0)
else:
setattr(self, "_" + key, dt.timestamp() * 1000.0)
@property
def type(self):
"""Returns the game's type. This is for compatibility with :class:`Activity`.
It always returns :attr:`ActivityType.playing`.
"""
return ActivityType.playing
@property
def start(self):
"""Optional[:class:`datetime.datetime`]: When the user started playing this game in UTC, if applicable."""
if self._start:
return datetime.datetime.utcfromtimestamp(self._start / 1000)
return None
@property
def end(self):
"""Optional[:class:`datetime.datetime`]: When the user will stop playing this game in UTC, if applicable."""
if self._end:
return datetime.datetime.utcfromtimestamp(self._end / 1000)
return None
def __str__(self):
return str(self.name)
def __repr__(self):
return "<Game name={0.name!r}>".format(self)
def to_dict(self):
timestamps = {}
if self._start:
timestamps["start"] = self._start
if self._end:
timestamps["end"] = self._end
return {
"type": ActivityType.playing.value,
"name": str(self.name),
"timestamps": timestamps,
}
def __eq__(self, other):
return isinstance(other, Game) and other.name == self.name
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self.name)
class Streaming(_ActivityTag):
"""A slimmed down version of :class:`Activity` that represents a Discord streaming status.
This is typically displayed via **Streaming** on the official Discord client.
.. container:: operations
.. describe:: x == y
Checks if two streams are equal.
.. describe:: x != y
Checks if two streams are not equal.
.. describe:: hash(x)
Returns the stream's hash.
.. describe:: str(x)
Returns the stream's name.
Attributes
-----------
name: :class:`str`
The stream's name.
url: :class:`str`
The stream's URL. Currently only twitch.tv URLs are supported. Anything else is silently
discarded.
details: Optional[:class:`str`]
If provided, typically the game the streamer is playing.
assets: :class:`dict`
A dictionary comprising of similar keys than those in :attr:`Activity.assets`.
"""
__slots__ = ("name", "url", "details", "assets")
def __init__(self, *, name, url, **extra):
self.name = name
self.url = url
self.details = extra.pop("details", None)
self.assets = extra.pop("assets", {})
@property
def type(self):
"""Returns the game's type. This is for compatibility with :class:`Activity`.
It always returns :attr:`ActivityType.streaming`.
"""
return ActivityType.streaming
def __str__(self):
return str(self.name)
def __repr__(self):
return "<Streaming name={0.name!r}>".format(self)
@property
def twitch_name(self):
"""Optional[:class:`str`]: If provided, the twitch name of the user streaming.
This corresponds to the ``large_image`` key of the :attr:`Streaming.assets`
dictionary if it starts with ``twitch:``. Typically set by the Discord client.
"""
try:
name = self.assets["large_image"]
except KeyError:
return None
else:
return name[7:] if name[:7] == "twitch:" else None
def to_dict(self):
ret = {
"type": ActivityType.streaming.value,
"name": str(self.name),
"url": str(self.url),
"assets": self.assets,
}
if self.details:
ret["details"] = self.details
return ret
def __eq__(self, other):
return isinstance(other, Streaming) and other.name == self.name and other.url == self.url
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self.name)
class Spotify:
"""Represents a Spotify listening activity from Discord. This is a special case of
:class:`Activity` that makes it easier to work with the Spotify integration.
.. container:: operations
.. describe:: x == y
Checks if two activities are equal.
.. describe:: x != y
Checks if two activities are not equal.
.. describe:: hash(x)
Returns the activity's hash.
.. describe:: str(x)
Returns the string 'Spotify'.
"""
__slots__ = (
"_state",
"_details",
"_timestamps",
"_assets",
"_party",
"_sync_id",
"_session_id",
)
def __init__(self, **data):
self._state = data.pop("state", None)
self._details = data.pop("details", None)
self._timestamps = data.pop("timestamps", {})
self._assets = data.pop("assets", {})
self._party = data.pop("party", {})
self._sync_id = data.pop("sync_id")
self._session_id = data.pop("session_id")
@property
def type(self):
"""Returns the activity's type. This is for compatibility with :class:`Activity`.
It always returns :attr:`ActivityType.listening`.
"""
return ActivityType.listening
@property
def colour(self):
"""Returns the Spotify integration colour, as a :class:`Colour`.
There is an alias for this named :meth:`color`"""
return Colour(0x1DB954)
@property
def color(self):
"""Returns the Spotify integration colour, as a :class:`Colour`.
There is an alias for this named :meth:`colour`"""
return self.colour
def to_dict(self):
return {
"flags": 48, # SYNC | PLAY
"name": "Spotify",
"assets": self._assets,
"party": self._party,
"sync_id": self._sync_id,
"session_id": self._session_id,
"timestamps": self._timestamps,
"details": self._details,
"state": self._state,
}
@property
def name(self):
""":class:`str`: The activity's name. This will always return "Spotify"."""
return "Spotify"
def __eq__(self, other):
return isinstance(other, Spotify) and other._session_id == self._session_id
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self._session_id)
def __str__(self):
return "Spotify"
def __repr__(self):
return "<Spotify title={0.title!r} artist={0.artist!r} track_id={0.track_id!r}>".format(
self
)
@property
def title(self):
""":class:`str`: The title of the song being played."""
return self._details
@property
def artists(self):
"""List[:class:`str`]: The artists of the song being played."""
return self._state.split("; ")
@property
def artist(self):
""":class:`str`: The artist of the song being played.
This does not attempt to split the artist information into
multiple artists. Useful if there's only a single artist.
"""
return self._state
@property
def album(self):
""":class:`str`: The album that the song being played belongs to."""
return self._assets.get("large_text", "")
@property
def album_cover_url(self):
""":class:`str`: The album cover image URL from Spotify's CDN."""
large_image = self._assets.get("large_image", "")
if large_image[:8] != "spotify:":
return ""
album_image_id = large_image[8:]
return "https://i.scdn.co/image/" + album_image_id
@property
def track_id(self):
""":class:`str`: The track ID used by Spotify to identify this song."""
return self._sync_id
@property
def start(self):
""":class:`datetime.datetime`: When the user started playing this song in UTC."""
return datetime.datetime.utcfromtimestamp(self._timestamps["start"] / 1000)
@property
def end(self):
""":class:`datetime.datetime`: When the user will stop playing this song in UTC."""
return datetime.datetime.utcfromtimestamp(self._timestamps["end"] / 1000)
@property
def duration(self):
""":class:`datetime.timedelta`: The duration of the song being played."""
return self.end - self.start
@property
def party_id(self):
""":class:`str`: The party ID of the listening party."""
return self._party.get("id", "")
def create_activity(data):
if not data:
return None
game_type = try_enum(ActivityType, data.get("type", -1))
if game_type is ActivityType.playing:
if "application_id" in data or "session_id" in data:
return Activity(**data)
return Game(**data)
elif game_type is ActivityType.streaming:
if "url" in data:
return Streaming(**data)
return Activity(**data)
elif game_type is ActivityType.listening and "sync_id" in data and "session_id" in data:
return Spotify(**data)
return Activity(**data)

366
discord/audit_logs.py Normal file
View File

@@ -0,0 +1,366 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from . import utils, enums
from .object import Object
from .permissions import PermissionOverwrite, Permissions
from .colour import Colour
from .invite import Invite
def _transform_verification_level(entry, data):
return enums.try_enum(enums.VerificationLevel, data)
def _transform_default_notifications(entry, data):
return enums.try_enum(enums.NotificationLevel, data)
def _transform_explicit_content_filter(entry, data):
return enums.try_enum(enums.ContentFilter, data)
def _transform_permissions(entry, data):
return Permissions(data)
def _transform_color(entry, data):
return Colour(data)
def _transform_snowflake(entry, data):
return int(data)
def _transform_channel(entry, data):
if data is None:
return None
channel = entry.guild.get_channel(int(data)) or Object(id=data)
return channel
def _transform_owner_id(entry, data):
if data is None:
return None
return entry._get_member(int(data))
def _transform_inviter_id(entry, data):
if data is None:
return None
return entry._get_member(int(data))
def _transform_overwrites(entry, data):
overwrites = []
for elem in data:
allow = Permissions(elem["allow"])
deny = Permissions(elem["deny"])
ow = PermissionOverwrite.from_pair(allow, deny)
ow_type = elem["type"]
ow_id = int(elem["id"])
if ow_type == "role":
target = entry.guild.get_role(ow_id)
else:
target = entry._get_member(ow_id)
if target is None:
target = Object(id=ow_id)
overwrites.append((target, ow))
return overwrites
class AuditLogDiff:
def __len__(self):
return len(self.__dict__)
def __iter__(self):
return iter(self.__dict__.items())
def __repr__(self):
return "<AuditLogDiff attrs={0!r}>".format(tuple(self.__dict__))
class AuditLogChanges:
TRANSFORMERS = {
"verification_level": (None, _transform_verification_level),
"explicit_content_filter": (None, _transform_explicit_content_filter),
"allow": (None, _transform_permissions),
"deny": (None, _transform_permissions),
"permissions": (None, _transform_permissions),
"id": (None, _transform_snowflake),
"color": ("colour", _transform_color),
"owner_id": ("owner", _transform_owner_id),
"inviter_id": ("inviter", _transform_inviter_id),
"channel_id": ("channel", _transform_channel),
"afk_channel_id": ("afk_channel", _transform_channel),
"system_channel_id": ("system_channel", _transform_channel),
"widget_channel_id": ("widget_channel", _transform_channel),
"permission_overwrites": ("overwrites", _transform_overwrites),
"splash_hash": ("splash", None),
"icon_hash": ("icon", None),
"avatar_hash": ("avatar", None),
"rate_limit_per_user": ("slowmode_delay", None),
"default_message_notifications": (
"default_notifications",
_transform_default_notifications,
),
}
def __init__(self, entry, data):
self.before = AuditLogDiff()
self.after = AuditLogDiff()
for elem in data:
attr = elem["key"]
# special cases for role add/remove
if attr == "$add":
self._handle_role(self.before, self.after, entry, elem["new_value"])
continue
elif attr == "$remove":
self._handle_role(self.after, self.before, entry, elem["new_value"])
continue
transformer = self.TRANSFORMERS.get(attr)
if transformer:
key, transformer = transformer
if key:
attr = key
try:
before = elem["old_value"]
except KeyError:
before = None
else:
if transformer:
before = transformer(entry, before)
setattr(self.before, attr, before)
try:
after = elem["new_value"]
except KeyError:
after = None
else:
if transformer:
after = transformer(entry, after)
setattr(self.after, attr, after)
# add an alias
if hasattr(self.after, "colour"):
self.after.color = self.after.colour
self.before.color = self.before.colour
def _handle_role(self, first, second, entry, elem):
if not hasattr(first, "roles"):
setattr(first, "roles", [])
data = []
g = entry.guild
for e in elem:
role_id = int(e["id"])
role = g.get_role(role_id)
if role is None:
role = Object(id=role_id)
role.name = e["name"]
data.append(role)
setattr(second, "roles", data)
class AuditLogEntry:
r"""Represents an Audit Log entry.
You retrieve these via :meth:`Guild.audit_logs`.
Attributes
-----------
action: :class:`AuditLogAction`
The action that was done.
user: :class:`abc.User`
The user who initiated this action. Usually a :class:`Member`\, unless gone
then it's a :class:`User`.
id: :class:`int`
The entry ID.
target: Any
The target that got changed. The exact type of this depends on
the action being done.
reason: Optional[:class:`str`]
The reason this action was done.
extra: Any
Extra information that this entry has that might be useful.
For most actions, this is ``None``. However in some cases it
contains extra information. See :class:`AuditLogAction` for
which actions have this field filled out.
"""
def __init__(self, *, users, data, guild):
self._state = guild._state
self.guild = guild
self._users = users
self._from_data(data)
def _from_data(self, data):
self.action = enums.AuditLogAction(data["action_type"])
self.id = int(data["id"])
# this key is technically not usually present
self.reason = data.get("reason")
self.extra = data.get("options")
if self.extra:
if self.action is enums.AuditLogAction.member_prune:
# member prune has two keys with useful information
self.extra = type(
"_AuditLogProxy", (), {k: int(v) for k, v in self.extra.items()}
)()
elif self.action is enums.AuditLogAction.message_delete:
channel_id = int(self.extra["channel_id"])
elems = {
"count": int(self.extra["count"]),
"channel": self.guild.get_channel(channel_id) or Object(id=channel_id),
}
self.extra = type("_AuditLogProxy", (), elems)()
elif self.action.name.startswith("overwrite_"):
# the overwrite_ actions have a dict with some information
instance_id = int(self.extra["id"])
the_type = self.extra.get("type")
if the_type == "member":
self.extra = self._get_member(instance_id)
else:
role = self.guild.get_role(instance_id)
if role is None:
role = Object(id=instance_id)
role.name = self.extra.get("role_name")
self.extra = role
# this key is not present when the above is present, typically.
# It's a list of { new_value: a, old_value: b, key: c }
# where new_value and old_value are not guaranteed to be there depending
# on the action type, so let's just fetch it for now and only turn it
# into meaningful data when requested
self._changes = data.get("changes", [])
self.user = self._get_member(utils._get_as_snowflake(data, "user_id"))
self._target_id = utils._get_as_snowflake(data, "target_id")
def _get_member(self, user_id):
return self.guild.get_member(user_id) or self._users.get(user_id)
def __repr__(self):
return "<AuditLogEntry id={0.id} action={0.action} user={0.user!r}>".format(self)
@utils.cached_property
def created_at(self):
"""Returns the entry's creation time in UTC."""
return utils.snowflake_time(self.id)
@utils.cached_property
def target(self):
try:
converter = getattr(self, "_convert_target_" + self.action.target_type)
except AttributeError:
return Object(id=self._target_id)
else:
return converter(self._target_id)
@utils.cached_property
def category(self):
"""Optional[:class:`AuditLogActionCategory`]: The category of the action, if applicable."""
return self.action.category
@utils.cached_property
def changes(self):
""":class:`AuditLogChanges`: The list of changes this entry has."""
obj = AuditLogChanges(self, self._changes)
del self._changes
return obj
@utils.cached_property
def before(self):
""":class:`AuditLogDiff`: The target's prior state."""
return self.changes.before
@utils.cached_property
def after(self):
""":class:`AuditLogDiff`: The target's subsequent state."""
return self.changes.after
def _convert_target_guild(self, target_id):
return self.guild
def _convert_target_channel(self, target_id):
ch = self.guild.get_channel(target_id)
if ch is None:
return Object(id=target_id)
return ch
def _convert_target_user(self, target_id):
return self._get_member(target_id)
def _convert_target_role(self, target_id):
role = self.guild.get_role(target_id)
if role is None:
return Object(id=target_id)
return role
def _convert_target_invite(self, target_id):
# invites have target_id set to null
# so figure out which change has the full invite data
changeset = (
self.before if self.action is enums.AuditLogAction.invite_delete else self.after
)
fake_payload = {
"max_age": changeset.max_age,
"max_uses": changeset.max_uses,
"code": changeset.code,
"temporary": changeset.temporary,
"channel": changeset.channel,
"uses": changeset.uses,
"guild": self.guild,
}
obj = Invite(state=self._state, data=fake_payload)
try:
obj.inviter = changeset.inviter
except AttributeError:
pass
return obj
def _convert_target_emoji(self, target_id):
return self._state.get_emoji(target_id) or Object(id=target_id)
def _convert_target_message(self, target_id):
return self._get_member(target_id)

86
discord/backoff.py Normal file
View File

@@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import time
import random
class ExponentialBackoff:
"""An implementation of the exponential backoff algorithm
Provides a convenient interface to implement an exponential backoff
for reconnecting or retrying transmissions in a distributed network.
Once instantiated, the delay method will return the next interval to
wait for when retrying a connection or transmission. The maximum
delay increases exponentially with each retry up to a maximum of
2^10 * base, and is reset if no more attempts are needed in a period
of 2^11 * base seconds.
Parameters
----------
base: int
The base delay in seconds. The first retry-delay will be up to
this many seconds.
integral: bool
Set to True if whole periods of base is desirable, otherwise any
number in between may be returned.
"""
def __init__(self, base=1, *, integral=False):
self._base = base
self._exp = 0
self._max = 10
self._reset_time = base * 2 ** 11
self._last_invocation = time.monotonic()
# Use our own random instance to avoid messing with global one
rand = random.Random()
rand.seed()
self._randfunc = rand.randrange if integral else rand.uniform
def delay(self):
"""Compute the next delay
Returns the next delay to wait according to the exponential
backoff algorithm. This is a value between 0 and base * 2^exp
where exponent starts off at 1 and is incremented at every
invocation of this method up to a maximum of 10.
If a period of more than base * 2^11 has passed since the last
retry, the exponent is reset to 1.
"""
invocation = time.monotonic()
interval = invocation - self._last_invocation
self._last_invocation = invocation
if interval > self._reset_time:
self._exp = 0
self._exp = min(self._exp + 1, self._max)
return self._randfunc(0, self._base * 2 ** self._exp)

157
discord/calls.py Normal file
View File

@@ -0,0 +1,157 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import datetime
from . import utils
from .enums import VoiceRegion, try_enum
from .member import VoiceState
class CallMessage:
"""Represents a group call message from Discord.
This is only received in cases where the message type is equivalent to
:attr:`MessageType.call`.
Attributes
-----------
ended_timestamp: Optional[datetime.datetime]
A naive UTC datetime object that represents the time that the call has ended.
participants: List[:class:`User`]
The list of users that are participating in this call.
message: :class:`Message`
The message associated with this call message.
"""
def __init__(self, message, **kwargs):
self.message = message
self.ended_timestamp = utils.parse_time(kwargs.get("ended_timestamp"))
self.participants = kwargs.get("participants")
@property
def call_ended(self):
""":obj:`bool`: Indicates if the call has ended."""
return self.ended_timestamp is not None
@property
def channel(self):
r""":class:`GroupChannel`\: The private channel associated with this message."""
return self.message.channel
@property
def duration(self):
"""Queries the duration of the call.
If the call has not ended then the current duration will
be returned.
Returns
---------
datetime.timedelta
The timedelta object representing the duration.
"""
if self.ended_timestamp is None:
return datetime.datetime.utcnow() - self.message.created_at
else:
return self.ended_timestamp - self.message.created_at
class GroupCall:
"""Represents the actual group call from Discord.
This is accompanied with a :class:`CallMessage` denoting the information.
Attributes
-----------
call: :class:`CallMessage`
The call message associated with this group call.
unavailable: :obj:`bool`
Denotes if this group call is unavailable.
ringing: List[:class:`User`]
A list of users that are currently being rung to join the call.
region: :class:`VoiceRegion`
The guild region the group call is being hosted on.
"""
def __init__(self, **kwargs):
self.call = kwargs.get("call")
self.unavailable = kwargs.get("unavailable")
self._voice_states = {}
for state in kwargs.get("voice_states", []):
self._update_voice_state(state)
self._update(**kwargs)
def _update(self, **kwargs):
self.region = try_enum(VoiceRegion, kwargs.get("region"))
lookup = {u.id: u for u in self.call.channel.recipients}
me = self.call.channel.me
lookup[me.id] = me
self.ringing = list(filter(None, map(lookup.get, kwargs.get("ringing", []))))
def _update_voice_state(self, data):
user_id = int(data["user_id"])
# left the voice channel?
if data["channel_id"] is None:
self._voice_states.pop(user_id, None)
else:
self._voice_states[user_id] = VoiceState(data=data, channel=self.channel)
@property
def connected(self):
"""A property that returns the :obj:`list` of :class:`User` that are currently in this call."""
ret = [u for u in self.channel.recipients if self.voice_state_for(u) is not None]
me = self.channel.me
if self.voice_state_for(me) is not None:
ret.append(me)
return ret
@property
def channel(self):
r""":class:`GroupChannel`\: Returns the channel the group call is in."""
return self.call.channel
def voice_state_for(self, user):
"""Retrieves the :class:`VoiceState` for a specified :class:`User`.
If the :class:`User` has no voice state then this function returns
``None``.
Parameters
------------
user: :class:`User`
The user to retrieve the voice state for.
Returns
--------
Optional[:class:`VoiceState`]
The voice state associated with this user.
"""
return self._voice_states.get(user.id)

1008
discord/channel.py Normal file

File diff suppressed because it is too large Load Diff

1074
discord/client.py Normal file

File diff suppressed because it is too large Load Diff

234
discord/colour.py Normal file
View File

@@ -0,0 +1,234 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import colorsys
class Colour:
"""Represents a Discord role colour. This class is similar
to an (red, green, blue) :class:`tuple`.
There is an alias for this called Color.
.. container:: operations
.. describe:: x == y
Checks if two colours are equal.
.. describe:: x != y
Checks if two colours are not equal.
.. describe:: hash(x)
Return the colour's hash.
.. describe:: str(x)
Returns the hex format for the colour.
Attributes
------------
value: :class:`int`
The raw integer colour value.
"""
__slots__ = ("value",)
def __init__(self, value):
if not isinstance(value, int):
raise TypeError(
"Expected int parameter, received %s instead." % value.__class__.__name__
)
self.value = value
def _get_byte(self, byte):
return (self.value >> (8 * byte)) & 0xFF
def __eq__(self, other):
return isinstance(other, Colour) and self.value == other.value
def __ne__(self, other):
return not self.__eq__(other)
def __str__(self):
return "#{:0>6x}".format(self.value)
def __repr__(self):
return "<Colour value=%s>" % self.value
def __hash__(self):
return hash(self.value)
@property
def r(self):
"""Returns the red component of the colour."""
return self._get_byte(2)
@property
def g(self):
"""Returns the green component of the colour."""
return self._get_byte(1)
@property
def b(self):
"""Returns the blue component of the colour."""
return self._get_byte(0)
def to_rgb(self):
"""Returns an (r, g, b) tuple representing the colour."""
return (self.r, self.g, self.b)
@classmethod
def from_rgb(cls, r, g, b):
"""Constructs a :class:`Colour` from an RGB tuple."""
return cls((r << 16) + (g << 8) + b)
@classmethod
def from_hsv(cls, h, s, v):
"""Constructs a :class:`Colour` from an HSV tuple."""
rgb = colorsys.hsv_to_rgb(h, s, v)
return cls.from_rgb(*(int(x * 255) for x in rgb))
@classmethod
def default(cls):
"""A factory method that returns a :class:`Colour` with a value of 0."""
return cls(0)
@classmethod
def teal(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x1abc9c``."""
return cls(0x1ABC9C)
@classmethod
def dark_teal(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x11806a``."""
return cls(0x11806A)
@classmethod
def green(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x2ecc71``."""
return cls(0x2ECC71)
@classmethod
def dark_green(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x1f8b4c``."""
return cls(0x1F8B4C)
@classmethod
def blue(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x3498db``."""
return cls(0x3498DB)
@classmethod
def dark_blue(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x206694``."""
return cls(0x206694)
@classmethod
def purple(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x9b59b6``."""
return cls(0x9B59B6)
@classmethod
def dark_purple(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x71368a``."""
return cls(0x71368A)
@classmethod
def magenta(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0xe91e63``."""
return cls(0xE91E63)
@classmethod
def dark_magenta(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0xad1457``."""
return cls(0xAD1457)
@classmethod
def gold(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0xf1c40f``."""
return cls(0xF1C40F)
@classmethod
def dark_gold(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0xc27c0e``."""
return cls(0xC27C0E)
@classmethod
def orange(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0xe67e22``."""
return cls(0xE67E22)
@classmethod
def dark_orange(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0xa84300``."""
return cls(0xA84300)
@classmethod
def red(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``."""
return cls(0xE74C3C)
@classmethod
def dark_red(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x992d22``."""
return cls(0x992D22)
@classmethod
def lighter_grey(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x95a5a6``."""
return cls(0x95A5A6)
@classmethod
def dark_grey(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x607d8b``."""
return cls(0x607D8B)
@classmethod
def light_grey(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x979c9f``."""
return cls(0x979C9F)
@classmethod
def darker_grey(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x546e7a``."""
return cls(0x546E7A)
@classmethod
def blurple(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x7289da``."""
return cls(0x7289DA)
@classmethod
def greyple(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0x99aab5``."""
return cls(0x99AAB5)
Color = Colour

View File

@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import asyncio
def _typing_done_callback(fut):
# just retrieve any exception and call it a day
try:
fut.exception()
except Exception:
pass
class Typing:
def __init__(self, messageable):
self.loop = messageable._state.loop
self.messageable = messageable
async def do_typing(self):
try:
channel = self._channel
except AttributeError:
channel = await self.messageable._get_channel()
typing = channel._state.http.send_typing
while True:
await typing(channel.id)
await asyncio.sleep(5)
def __enter__(self):
self.task = asyncio.ensure_future(self.do_typing(), loop=self.loop)
self.task.add_done_callback(_typing_done_callback)
return self
def __exit__(self, exc_type, exc, tb):
self.task.cancel()
async def __aenter__(self):
self._channel = channel = await self.messageable._get_channel()
await channel._state.http.send_typing(channel.id)
return self.__enter__()
async def __aexit__(self, exc_type, exc, tb):
self.task.cancel()

492
discord/embeds.py Normal file
View File

@@ -0,0 +1,492 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import datetime
from . import utils
from .colour import Colour
class _EmptyEmbed:
def __bool__(self):
return False
def __repr__(self):
return "Embed.Empty"
EmptyEmbed = _EmptyEmbed()
class EmbedProxy:
def __init__(self, layer):
self.__dict__.update(layer)
def __len__(self):
return len(self.__dict__)
def __repr__(self):
return "EmbedProxy(%s)" % ", ".join(
("%s=%r" % (k, v) for k, v in self.__dict__.items() if not k.startswith("_"))
)
def __getattr__(self, attr):
return EmptyEmbed
class Embed:
"""Represents a Discord embed.
The following attributes can be set during creation
of the object:
Certain properties return an ``EmbedProxy``. Which is a type
that acts similar to a regular :class:`dict` except access the attributes
via dotted access, e.g. ``embed.author.icon_url``. If the attribute
is invalid or empty, then a special sentinel value is returned,
:attr:`Embed.Empty`.
For ease of use, all parameters that expect a :class:`str` are implicitly
casted to :class:`str` for you.
Attributes
-----------
title: :class:`str`
The title of the embed.
type: :class:`str`
The type of embed. Usually "rich".
description: :class:`str`
The description of the embed.
url: :class:`str`
The URL of the embed.
timestamp: `datetime.datetime`
The timestamp of the embed content. This could be a naive or aware datetime.
colour: :class:`Colour` or :class:`int`
The colour code of the embed. Aliased to ``color`` as well.
Empty
A special sentinel value used by ``EmbedProxy`` and this class
to denote that the value or attribute is empty.
"""
__slots__ = (
"title",
"url",
"type",
"_timestamp",
"_colour",
"_footer",
"_image",
"_thumbnail",
"_video",
"_provider",
"_author",
"_fields",
"description",
)
Empty = EmptyEmbed
def __init__(self, **kwargs):
# swap the colour/color aliases
try:
colour = kwargs["colour"]
except KeyError:
colour = kwargs.get("color", EmptyEmbed)
self.colour = colour
self.title = kwargs.get("title", EmptyEmbed)
self.type = kwargs.get("type", "rich")
self.url = kwargs.get("url", EmptyEmbed)
self.description = kwargs.get("description", EmptyEmbed)
try:
timestamp = kwargs["timestamp"]
except KeyError:
pass
else:
self.timestamp = timestamp
@classmethod
def from_data(cls, data):
# we are bypassing __init__ here since it doesn't apply here
self = cls.__new__(cls)
# fill in the basic fields
self.title = data.get("title", EmptyEmbed)
self.type = data.get("type", EmptyEmbed)
self.description = data.get("description", EmptyEmbed)
self.url = data.get("url", EmptyEmbed)
# try to fill in the more rich fields
try:
self._colour = Colour(value=data["color"])
except KeyError:
pass
try:
self._timestamp = utils.parse_time(data["timestamp"])
except KeyError:
pass
for attr in ("thumbnail", "video", "provider", "author", "fields", "image", "footer"):
try:
value = data[attr]
except KeyError:
continue
else:
setattr(self, "_" + attr, value)
return self
@property
def colour(self):
return getattr(self, "_colour", EmptyEmbed)
@colour.setter
def colour(self, value):
if isinstance(value, (Colour, _EmptyEmbed)):
self._colour = value
elif isinstance(value, int):
self._colour = Colour(value=value)
else:
raise TypeError(
"Expected discord.Colour, int, or Embed.Empty but received %s instead."
% value.__class__.__name__
)
color = colour
@property
def timestamp(self):
return getattr(self, "_timestamp", EmptyEmbed)
@timestamp.setter
def timestamp(self, value):
if isinstance(value, (datetime.datetime, _EmptyEmbed)):
self._timestamp = value
else:
raise TypeError(
"Expected datetime.datetime or Embed.Empty received %s instead"
% value.__class__.__name__
)
@property
def footer(self):
"""Returns an ``EmbedProxy`` denoting the footer contents.
See :meth:`set_footer` for possible values you can access.
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, "_footer", {}))
def set_footer(self, *, text=EmptyEmbed, icon_url=EmptyEmbed):
"""Sets the footer for the embed content.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
-----------
text: str
The footer text.
icon_url: str
The URL of the footer icon. Only HTTP(S) is supported.
"""
self._footer = {}
if text is not EmptyEmbed:
self._footer["text"] = str(text)
if icon_url is not EmptyEmbed:
self._footer["icon_url"] = str(icon_url)
return self
@property
def image(self):
"""Returns an ``EmbedProxy`` denoting the image contents.
Possible attributes you can access are:
- ``url``
- ``proxy_url``
- ``width``
- ``height``
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, "_image", {}))
def set_image(self, *, url):
"""Sets the image for the embed content.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
-----------
url: str
The source URL for the image. Only HTTP(S) is supported.
"""
self._image = {"url": str(url)}
return self
@property
def thumbnail(self):
"""Returns an ``EmbedProxy`` denoting the thumbnail contents.
Possible attributes you can access are:
- ``url``
- ``proxy_url``
- ``width``
- ``height``
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, "_thumbnail", {}))
def set_thumbnail(self, *, url):
"""Sets the thumbnail for the embed content.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
-----------
url: str
The source URL for the thumbnail. Only HTTP(S) is supported.
"""
self._thumbnail = {"url": str(url)}
return self
@property
def video(self):
"""Returns an ``EmbedProxy`` denoting the video contents.
Possible attributes include:
- ``url`` for the video URL.
- ``height`` for the video height.
- ``width`` for the video width.
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, "_video", {}))
@property
def provider(self):
"""Returns an ``EmbedProxy`` denoting the provider contents.
The only attributes that might be accessed are ``name`` and ``url``.
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, "_provider", {}))
@property
def author(self):
"""Returns an ``EmbedProxy`` denoting the author contents.
See :meth:`set_author` for possible values you can access.
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, "_author", {}))
def set_author(self, *, name, url=EmptyEmbed, icon_url=EmptyEmbed):
"""Sets the author for the embed content.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
-----------
name: str
The name of the author.
url: str
The URL for the author.
icon_url: str
The URL of the author icon. Only HTTP(S) is supported.
"""
self._author = {"name": str(name)}
if url is not EmptyEmbed:
self._author["url"] = str(url)
if icon_url is not EmptyEmbed:
self._author["icon_url"] = str(icon_url)
return self
@property
def fields(self):
"""Returns a :class:`list` of ``EmbedProxy`` denoting the field contents.
See :meth:`add_field` for possible values you can access.
If the attribute has no value then :attr:`Empty` is returned.
"""
return [EmbedProxy(d) for d in getattr(self, "_fields", [])]
def add_field(self, *, name, value, inline=True):
"""Adds a field to the embed object.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
-----------
name: str
The name of the field.
value: str
The value of the field.
inline: bool
Whether the field should be displayed inline.
"""
field = {"inline": inline, "name": str(name), "value": str(value)}
try:
self._fields.append(field)
except AttributeError:
self._fields = [field]
return self
def clear_fields(self):
"""Removes all fields from this embed."""
try:
self._fields.clear()
except AttributeError:
self._fields = []
def remove_field(self, index):
"""Removes a field at a specified index.
If the index is invalid or out of bounds then the error is
silently swallowed.
.. note::
When deleting a field by index, the index of the other fields
shift to fill the gap just like a regular list.
Parameters
-----------
index: int
The index of the field to remove.
"""
try:
del self._fields[index]
except (AttributeError, IndexError):
pass
def set_field_at(self, index, *, name, value, inline=True):
"""Modifies a field to the embed object.
The index must point to a valid pre-existing field.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
-----------
index: int
The index of the field to modify.
name: str
The name of the field.
value: str
The value of the field.
inline: bool
Whether the field should be displayed inline.
Raises
-------
IndexError
An invalid index was provided.
"""
try:
field = self._fields[index]
except (TypeError, IndexError, AttributeError):
raise IndexError("field index out of range")
field["name"] = str(name)
field["value"] = str(value)
field["inline"] = inline
return self
def to_dict(self):
"""Converts this embed object into a dict."""
# add in the raw data into the dict
result = {
key[1:]: getattr(self, key)
for key in self.__slots__
if key[0] == "_" and hasattr(self, key)
}
# deal with basic convenience wrappers
try:
colour = result.pop("colour")
except KeyError:
pass
else:
if colour:
result["color"] = colour.value
try:
timestamp = result.pop("timestamp")
except KeyError:
pass
else:
if timestamp:
result["timestamp"] = timestamp.isoformat()
# add in the non raw attribute ones
if self.type:
result["type"] = self.type
if self.description:
result["description"] = self.description
if self.url:
result["url"] = self.url
if self.title:
result["title"] = self.title
return result

279
discord/emoji.py Normal file
View File

@@ -0,0 +1,279 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from collections import namedtuple
from . import utils
from .mixins import Hashable
class PartialEmoji(namedtuple("PartialEmoji", "animated name id")):
"""Represents a "partial" emoji.
This model will be given in two scenarios:
- "Raw" data events such as :func:`on_raw_reaction_add`
- Custom emoji that the bot cannot see from e.g. :attr:`Message.reactions`
.. container:: operations
.. describe:: x == y
Checks if two emoji are the same.
.. describe:: x != y
Checks if two emoji are not the same.
.. describe:: hash(x)
Return the emoji's hash.
.. describe:: str(x)
Returns the emoji rendered for discord.
Attributes
-----------
name: :class:`str`
The custom emoji name, if applicable, or the unicode codepoint
of the non-custom emoji.
animated: :class:`bool`
Whether the emoji is animated or not.
id: Optional[:class:`int`]
The ID of the custom emoji, if applicable.
"""
__slots__ = ()
def __str__(self):
if self.id is None:
return self.name
if self.animated:
return "<a:%s:%s>" % (self.name, self.id)
return "<:%s:%s>" % (self.name, self.id)
def __eq__(self, other):
if self.is_unicode_emoji():
return isinstance(other, PartialEmoji) and self.name == other.name
if isinstance(other, (PartialEmoji, Emoji)):
return self.id == other.id
def is_custom_emoji(self):
"""Checks if this is a custom non-Unicode emoji."""
return self.id is not None
def is_unicode_emoji(self):
"""Checks if this is a Unicode emoji."""
return self.id is None
def _as_reaction(self):
if self.id is None:
return self.name
return "%s:%s" % (self.name, self.id)
@property
def url(self):
"""Returns a URL version of the emoji, if it is custom."""
if self.is_unicode_emoji():
return None
_format = "gif" if self.animated else "png"
return "https://cdn.discordapp.com/emojis/{0.id}.{1}".format(self, _format)
class Emoji(Hashable):
"""Represents a custom emoji.
Depending on the way this object was created, some of the attributes can
have a value of ``None``.
.. container:: operations
.. describe:: x == y
Checks if two emoji are the same.
.. describe:: x != y
Checks if two emoji are not the same.
.. describe:: hash(x)
Return the emoji's hash.
.. describe:: iter(x)
Returns an iterator of ``(field, value)`` pairs. This allows this class
to be used as an iterable in list/dict/etc constructions.
.. describe:: str(x)
Returns the emoji rendered for discord.
Attributes
-----------
name: :class:`str`
The name of the emoji.
id: :class:`int`
The emoji's ID.
require_colons: :class:`bool`
If colons are required to use this emoji in the client (:PJSalt: vs PJSalt).
animated: :class:`bool`
Whether an emoji is animated or not.
managed: :class:`bool`
If this emoji is managed by a Twitch integration.
guild_id: :class:`int`
The guild ID the emoji belongs to.
"""
__slots__ = (
"require_colons",
"animated",
"managed",
"id",
"name",
"_roles",
"guild_id",
"_state",
)
def __init__(self, *, guild, state, data):
self.guild_id = guild.id
self._state = state
self._from_data(data)
def _from_data(self, emoji):
self.require_colons = emoji["require_colons"]
self.managed = emoji["managed"]
self.id = int(emoji["id"])
self.name = emoji["name"]
self.animated = emoji.get("animated", False)
self._roles = utils.SnowflakeList(map(int, emoji.get("roles", [])))
def _iterator(self):
for attr in self.__slots__:
if attr[0] != "_":
value = getattr(self, attr, None)
if value is not None:
yield (attr, value)
def __iter__(self):
return self._iterator()
def __str__(self):
if self.animated:
return "<a:{0.name}:{0.id}>".format(self)
return "<:{0.name}:{0.id}>".format(self)
def __repr__(self):
return "<Emoji id={0.id} name={0.name!r}>".format(self)
def __eq__(self, other):
return isinstance(other, (PartialEmoji, Emoji)) and self.id == other.id
@property
def created_at(self):
"""Returns the emoji's creation time in UTC."""
return utils.snowflake_time(self.id)
@property
def url(self):
"""Returns a URL version of the emoji."""
_format = "gif" if self.animated else "png"
return "https://cdn.discordapp.com/emojis/{0.id}.{1}".format(self, _format)
@property
def roles(self):
"""List[:class:`Role`]: A :class:`list` of roles that is allowed to use this emoji.
If roles is empty, the emoji is unrestricted.
"""
guild = self.guild
if guild is None:
return []
return [role for role in guild.roles if self._roles.has(role.id)]
@property
def guild(self):
""":class:`Guild`: The guild this emoji belongs to."""
return self._state._get_guild(self.guild_id)
async def delete(self, *, reason=None):
"""|coro|
Deletes the custom emoji.
You must have :attr:`~Permissions.manage_emojis` permission to
do this.
Parameters
-----------
reason: Optional[str]
The reason for deleting this emoji. Shows up on the audit log.
Raises
-------
Forbidden
You are not allowed to delete emojis.
HTTPException
An error occurred deleting the emoji.
"""
await self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason)
async def edit(self, *, name, roles=None, reason=None):
r"""|coro|
Edits the custom emoji.
You must have :attr:`~Permissions.manage_emojis` permission to
do this.
Parameters
-----------
name: str
The new emoji name.
roles: Optional[list[:class:`Role`]]
A :class:`list` of :class:`Role`\s that can use this emoji. Leave empty to make it available to everyone.
reason: Optional[str]
The reason for editing this emoji. Shows up on the audit log.
Raises
-------
Forbidden
You are not allowed to edit emojis.
HTTPException
An error occurred editing the emoji.
"""
if roles:
roles = [role.id for role in roles]
await self._state.http.edit_custom_emoji(
self.guild.id, self.id, name=name, roles=roles, reason=reason
)

285
discord/enums.py Normal file
View File

@@ -0,0 +1,285 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from enum import Enum, IntEnum
__all__ = [
"ChannelType",
"MessageType",
"VoiceRegion",
"SpeakingState",
"VerificationLevel",
"ContentFilter",
"Status",
"DefaultAvatar",
"RelationshipType",
"AuditLogAction",
"AuditLogActionCategory",
"UserFlags",
"ActivityType",
"HypeSquadHouse",
"NotificationLevel",
]
class ChannelType(Enum):
text = 0
private = 1
voice = 2
group = 3
category = 4
def __str__(self):
return self.name
class MessageType(Enum):
default = 0
recipient_add = 1
recipient_remove = 2
call = 3
channel_name_change = 4
channel_icon_change = 5
pins_add = 6
new_member = 7
class VoiceRegion(Enum):
us_west = "us-west"
us_east = "us-east"
us_south = "us-south"
us_central = "us-central"
eu_west = "eu-west"
eu_central = "eu-central"
singapore = "singapore"
london = "london"
sydney = "sydney"
amsterdam = "amsterdam"
frankfurt = "frankfurt"
brazil = "brazil"
hongkong = "hongkong"
russia = "russia"
japan = "japan"
southafrica = "southafrica"
vip_us_east = "vip-us-east"
vip_us_west = "vip-us-west"
vip_amsterdam = "vip-amsterdam"
def __str__(self):
return self.value
class SpeakingState(IntEnum):
none = 0
voice = 1
soundshare = 2
priority = 4
def __str__(self):
return self.name
class VerificationLevel(IntEnum):
none = 0
low = 1
medium = 2
high = 3
table_flip = 3
extreme = 4
double_table_flip = 4
def __str__(self):
return self.name
class ContentFilter(IntEnum):
disabled = 0
no_role = 1
all_members = 2
def __str__(self):
return self.name
class Status(Enum):
online = "online"
offline = "offline"
idle = "idle"
dnd = "dnd"
do_not_disturb = "dnd"
invisible = "invisible"
def __str__(self):
return self.value
class DefaultAvatar(Enum):
blurple = 0
grey = 1
gray = 1
green = 2
orange = 3
red = 4
def __str__(self):
return self.name
class RelationshipType(Enum):
friend = 1
blocked = 2
incoming_request = 3
outgoing_request = 4
class NotificationLevel(IntEnum):
all_messages = 0
only_mentions = 1
class AuditLogActionCategory(Enum):
create = 1
delete = 2
update = 3
class AuditLogAction(Enum):
guild_update = 1
channel_create = 10
channel_update = 11
channel_delete = 12
overwrite_create = 13
overwrite_update = 14
overwrite_delete = 15
kick = 20
member_prune = 21
ban = 22
unban = 23
member_update = 24
member_role_update = 25
role_create = 30
role_update = 31
role_delete = 32
invite_create = 40
invite_update = 41
invite_delete = 42
webhook_create = 50
webhook_update = 51
webhook_delete = 52
emoji_create = 60
emoji_update = 61
emoji_delete = 62
message_delete = 72
@property
def category(self):
lookup = {
AuditLogAction.guild_update: AuditLogActionCategory.update,
AuditLogAction.channel_create: AuditLogActionCategory.create,
AuditLogAction.channel_update: AuditLogActionCategory.update,
AuditLogAction.channel_delete: AuditLogActionCategory.delete,
AuditLogAction.overwrite_create: AuditLogActionCategory.create,
AuditLogAction.overwrite_update: AuditLogActionCategory.update,
AuditLogAction.overwrite_delete: AuditLogActionCategory.delete,
AuditLogAction.kick: None,
AuditLogAction.member_prune: None,
AuditLogAction.ban: None,
AuditLogAction.unban: None,
AuditLogAction.member_update: AuditLogActionCategory.update,
AuditLogAction.member_role_update: AuditLogActionCategory.update,
AuditLogAction.role_create: AuditLogActionCategory.create,
AuditLogAction.role_update: AuditLogActionCategory.update,
AuditLogAction.role_delete: AuditLogActionCategory.delete,
AuditLogAction.invite_create: AuditLogActionCategory.create,
AuditLogAction.invite_update: AuditLogActionCategory.update,
AuditLogAction.invite_delete: AuditLogActionCategory.delete,
AuditLogAction.webhook_create: AuditLogActionCategory.create,
AuditLogAction.webhook_update: AuditLogActionCategory.update,
AuditLogAction.webhook_delete: AuditLogActionCategory.delete,
AuditLogAction.emoji_create: AuditLogActionCategory.create,
AuditLogAction.emoji_update: AuditLogActionCategory.update,
AuditLogAction.emoji_delete: AuditLogActionCategory.delete,
AuditLogAction.message_delete: AuditLogActionCategory.delete,
}
return lookup[self]
@property
def target_type(self):
v = self.value
if v == -1:
return "all"
elif v < 10:
return "guild"
elif v < 20:
return "channel"
elif v < 30:
return "user"
elif v < 40:
return "role"
elif v < 50:
return "invite"
elif v < 60:
return "webhook"
elif v < 70:
return "emoji"
elif v < 80:
return "message"
class UserFlags(Enum):
staff = 1
partner = 2
hypesquad = 4
bug_hunter = 8
hypesquad_bravery = 64
hypesquad_brilliance = 128
hypesquad_balance = 256
early_supporter = 512
class ActivityType(IntEnum):
unknown = -1
playing = 0
streaming = 1
listening = 2
watching = 3
class HypeSquadHouse(Enum):
bravery = 1
brilliance = 2
balance = 3
def try_enum(cls, val):
"""A function that tries to turn the value into enum ``cls``.
If it fails it returns the value instead.
"""
try:
return cls(val)
except ValueError:
return val

183
discord/errors.py Normal file
View File

@@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
class DiscordException(Exception):
"""Base exception class for discord.py
Ideally speaking, this could be caught to handle any exceptions thrown from this library.
"""
pass
class ClientException(DiscordException):
"""Exception that's thrown when an operation in the :class:`Client` fails.
These are usually for exceptions that happened due to user input.
"""
pass
class NoMoreItems(DiscordException):
"""Exception that is thrown when an async iteration operation has no more
items."""
pass
class GatewayNotFound(DiscordException):
"""An exception that is usually thrown when the gateway hub
for the :class:`Client` websocket is not found."""
def __init__(self):
message = "The gateway to connect to discord was not found."
super(GatewayNotFound, self).__init__(message)
def flatten_error_dict(d, key=""):
items = []
for k, v in d.items():
new_key = key + "." + k if key else k
if isinstance(v, dict):
try:
_errors = v["_errors"]
except KeyError:
items.extend(flatten_error_dict(v, new_key).items())
else:
items.append((new_key, " ".join(x.get("message", "") for x in _errors)))
else:
items.append((new_key, v))
return dict(items)
class HTTPException(DiscordException):
"""Exception that's thrown when an HTTP request operation fails.
Attributes
------------
response: aiohttp.ClientResponse
The response of the failed HTTP request. This is an
instance of `aiohttp.ClientResponse`__. In some cases
this could also be a ``requests.Response``.
__ http://aiohttp.readthedocs.org/en/stable/client_reference.html#aiohttp.ClientResponse
text: :class:`str`
The text of the error. Could be an empty string.
status: :class:`int`
The status code of the HTTP request.
code: :class:`int`
The Discord specific error code for the failure.
"""
def __init__(self, response, message):
self.response = response
self.status = response.status
if isinstance(message, dict):
self.code = message.get("code", 0)
base = message.get("message", "")
errors = message.get("errors")
if errors:
errors = flatten_error_dict(errors)
helpful = "\n".join("In %s: %s" % t for t in errors.items())
self.text = base + "\n" + helpful
else:
self.text = base
else:
self.text = message
self.code = 0
fmt = "{0.reason} (status code: {0.status})"
if len(self.text):
fmt = fmt + ": {1}"
super().__init__(fmt.format(self.response, self.text))
class Forbidden(HTTPException):
"""Exception that's thrown for when status code 403 occurs.
Subclass of :exc:`HTTPException`
"""
pass
class NotFound(HTTPException):
"""Exception that's thrown for when status code 404 occurs.
Subclass of :exc:`HTTPException`
"""
pass
class InvalidArgument(ClientException):
"""Exception that's thrown when an argument to a function
is invalid some way (e.g. wrong value or wrong type).
This could be considered the analogous of ``ValueError`` and
``TypeError`` except derived from :exc:`ClientException` and thus
:exc:`DiscordException`.
"""
pass
class LoginFailure(ClientException):
"""Exception that's thrown when the :meth:`Client.login` function
fails to log you in from improper credentials or some other misc.
failure.
"""
pass
class ConnectionClosed(ClientException):
"""Exception that's thrown when the gateway connection is
closed for reasons that could not be handled internally.
Attributes
-----------
code: :class:`int`
The close code of the websocket.
reason: :class:`str`
The reason provided for the closure.
shard_id: Optional[:class:`int`]
The shard ID that got closed if applicable.
"""
def __init__(self, original, *, shard_id):
# This exception is just the same exception except
# reconfigured to subclass ClientException for users
self.code = original.code
self.reason = original.reason
self.shard_id = shard_id
super().__init__(str(original))

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""
discord.ext.commands
~~~~~~~~~~~~~~~~~~~~~
An extension module to facilitate creation of bot commands.
:copyright: (c) 2019 Rapptz
:license: MIT, see LICENSE for more details.
"""
from .bot import Bot, AutoShardedBot, when_mentioned, when_mentioned_or
from .context import Context
from .core import *
from .errors import *
from .formatter import HelpFormatter, Paginator
from .converter import *
from .cooldowns import *

1049
discord/ext/commands/bot.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,225 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import discord.abc
import discord.utils
class Context(discord.abc.Messageable):
r"""Represents the context in which a command is being invoked under.
This class contains a lot of meta data to help you understand more about
the invocation context. This class is not created manually and is instead
passed around to commands as the first parameter.
This class implements the :class:`abc.Messageable` ABC.
Attributes
-----------
message: :class:`discord.Message`
The message that triggered the command being executed.
bot: :class:`.Bot`
The bot that contains the command being executed.
args: :class:`list`
The list of transformed arguments that were passed into the command.
If this is accessed during the :func:`on_command_error` event
then this list could be incomplete.
kwargs: :class:`dict`
A dictionary of transformed arguments that were passed into the command.
Similar to :attr:`args`\, if this is accessed in the
:func:`on_command_error` event then this dict could be incomplete.
prefix: :class:`str`
The prefix that was used to invoke the command.
command
The command (i.e. :class:`.Command` or its superclasses) that is being
invoked currently.
invoked_with: :class:`str`
The command name that triggered this invocation. Useful for finding out
which alias called the command.
invoked_subcommand
The subcommand (i.e. :class:`.Command` or its superclasses) that was
invoked. If no valid subcommand was invoked then this is equal to
`None`.
subcommand_passed: Optional[:class:`str`]
The string that was attempted to call a subcommand. This does not have
to point to a valid registered subcommand and could just point to a
nonsense string. If nothing was passed to attempt a call to a
subcommand then this is set to `None`.
command_failed: :class:`bool`
A boolean that indicates if the command failed to be parsed, checked,
or invoked.
"""
def __init__(self, **attrs):
self.message = attrs.pop("message", None)
self.bot = attrs.pop("bot", None)
self.args = attrs.pop("args", [])
self.kwargs = attrs.pop("kwargs", {})
self.prefix = attrs.pop("prefix")
self.command = attrs.pop("command", None)
self.view = attrs.pop("view", None)
self.invoked_with = attrs.pop("invoked_with", None)
self.invoked_subcommand = attrs.pop("invoked_subcommand", None)
self.subcommand_passed = attrs.pop("subcommand_passed", None)
self.command_failed = attrs.pop("command_failed", False)
self._state = self.message._state
async def invoke(self, *args, **kwargs):
r"""|coro|
Calls a command with the arguments given.
This is useful if you want to just call the callback that a
:class:`.Command` holds internally.
Note
------
You do not pass in the context as it is done for you.
Warning
---------
The first parameter passed **must** be the command being invoked.
Parameters
-----------
command: :class:`.Command`
A command or superclass of a command that is going to be called.
\*args
The arguments to to use.
\*\*kwargs
The keyword arguments to use.
"""
try:
command = args[0]
except IndexError:
raise TypeError("Missing command to invoke.") from None
arguments = []
if command.instance is not None:
arguments.append(command.instance)
arguments.append(self)
arguments.extend(args[1:])
ret = await command.callback(*arguments, **kwargs)
return ret
async def reinvoke(self, *, call_hooks=False, restart=True):
"""|coro|
Calls the command again.
This is similar to :meth:`~.Context.invoke` except that it bypasses
checks, cooldowns, and error handlers.
.. note::
If you want to bypass :exc:`.UserInputError` derived exceptions,
it is recommended to use the regular :meth:`~.Context.invoke`
as it will work more naturally. After all, this will end up
using the old arguments the user has used and will thus just
fail again.
Parameters
------------
call_hooks: bool
Whether to call the before and after invoke hooks.
restart: bool
Whether to start the call chain from the very beginning
or where we left off (i.e. the command that caused the error).
The default is to start where we left off.
"""
cmd = self.command
view = self.view
if cmd is None:
raise ValueError("This context is not valid.")
# some state to revert to when we're done
index, previous = view.index, view.previous
invoked_with = self.invoked_with
invoked_subcommand = self.invoked_subcommand
subcommand_passed = self.subcommand_passed
if restart:
to_call = cmd.root_parent or cmd
view.index = len(self.prefix)
view.previous = 0
view.get_word() # advance to get the root command
else:
to_call = cmd
try:
await to_call.reinvoke(self, call_hooks=call_hooks)
finally:
self.command = cmd
view.index = index
view.previous = previous
self.invoked_with = invoked_with
self.invoked_subcommand = invoked_subcommand
self.subcommand_passed = subcommand_passed
@property
def valid(self):
"""Checks if the invocation context is valid to be invoked with."""
return self.prefix is not None and self.command is not None
async def _get_channel(self):
return self.channel
@property
def cog(self):
"""Returns the cog associated with this context's command. None if it does not exist."""
if self.command is None:
return None
return self.command.instance
@discord.utils.cached_property
def guild(self):
"""Returns the guild associated with this context's command. None if not available."""
return self.message.guild
@discord.utils.cached_property
def channel(self):
"""Returns the channel associated with this context's command. Shorthand for :attr:`Message.channel`."""
return self.message.channel
@discord.utils.cached_property
def author(self):
"""Returns the author associated with this context's command. Shorthand for :attr:`Message.author`"""
return self.message.author
@discord.utils.cached_property
def me(self):
"""Similar to :attr:`Guild.me` except it may return the :class:`ClientUser` in private message contexts."""
return self.guild.me if self.guild is not None else self.bot.user
@property
def voice_client(self):
r"""Optional[:class:`VoiceClient`]: A shortcut to :attr:`Guild.voice_client`\, if applicable."""
g = self.guild
return g.voice_client if g else None

View File

@@ -0,0 +1,560 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import re
import inspect
import discord
from .errors import BadArgument, NoPrivateMessage
__all__ = [
"Converter",
"MemberConverter",
"UserConverter",
"TextChannelConverter",
"InviteConverter",
"RoleConverter",
"GameConverter",
"ColourConverter",
"VoiceChannelConverter",
"EmojiConverter",
"PartialEmojiConverter",
"CategoryChannelConverter",
"IDConverter",
"clean_content",
"Greedy",
]
def _get_from_guilds(bot, getter, argument):
result = None
for guild in bot.guilds:
result = getattr(guild, getter)(argument)
if result:
return result
return result
class Converter:
"""The base class of custom converters that require the :class:`.Context`
to be passed to be useful.
This allows you to implement converters that function similar to the
special cased ``discord`` classes.
Classes that derive from this should override the :meth:`~.Converter.convert`
method to do its conversion logic. This method must be a coroutine.
"""
async def convert(self, ctx, argument):
"""|coro|
The method to override to do conversion logic.
If an error is found while converting, it is recommended to
raise a :exc:`.CommandError` derived exception as it will
properly propagate to the error handlers.
Parameters
-----------
ctx: :class:`.Context`
The invocation context that the argument is being used in.
argument: str
The argument that is being converted.
"""
raise NotImplementedError("Derived classes need to implement this.")
class IDConverter(Converter):
def __init__(self):
self._id_regex = re.compile(r"([0-9]{15,21})$")
super().__init__()
def _get_id_match(self, argument):
return self._id_regex.match(argument)
class MemberConverter(IDConverter):
"""Converts to a :class:`Member`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name#discrim
4. Lookup by name
5. Lookup by nickname
"""
async def convert(self, ctx, argument):
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r"<@!?([0-9]+)>$", argument)
guild = ctx.guild
result = None
if match is None:
# not a mention...
if guild:
result = guild.get_member_named(argument)
else:
result = _get_from_guilds(bot, "get_member_named", argument)
else:
user_id = int(match.group(1))
if guild:
result = guild.get_member(user_id)
else:
result = _get_from_guilds(bot, "get_member", user_id)
if result is None:
raise BadArgument('Member "{}" not found'.format(argument))
return result
class UserConverter(IDConverter):
"""Converts to a :class:`User`.
All lookups are via the global user cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name#discrim
4. Lookup by name
"""
async def convert(self, ctx, argument):
match = self._get_id_match(argument) or re.match(r"<@!?([0-9]+)>$", argument)
result = None
state = ctx._state
if match is not None:
user_id = int(match.group(1))
result = ctx.bot.get_user(user_id)
else:
arg = argument
# check for discriminator if it exists
if len(arg) > 5 and arg[-5] == "#":
discrim = arg[-4:]
name = arg[:-5]
predicate = lambda u: u.name == name and u.discriminator == discrim
result = discord.utils.find(predicate, state._users.values())
if result is not None:
return result
predicate = lambda u: u.name == arg
result = discord.utils.find(predicate, state._users.values())
if result is None:
raise BadArgument('User "{}" not found'.format(argument))
return result
class TextChannelConverter(IDConverter):
"""Converts to a :class:`TextChannel`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name
"""
async def convert(self, ctx, argument):
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r"<#([0-9]+)>$", argument)
result = None
guild = ctx.guild
if match is None:
# not a mention
if guild:
result = discord.utils.get(guild.text_channels, name=argument)
else:
def check(c):
return isinstance(c, discord.TextChannel) and c.name == argument
result = discord.utils.find(check, bot.get_all_channels())
else:
channel_id = int(match.group(1))
if guild:
result = guild.get_channel(channel_id)
else:
result = _get_from_guilds(bot, "get_channel", channel_id)
if not isinstance(result, discord.TextChannel):
raise BadArgument('Channel "{}" not found.'.format(argument))
return result
class VoiceChannelConverter(IDConverter):
"""Converts to a :class:`VoiceChannel`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name
"""
async def convert(self, ctx, argument):
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r"<#([0-9]+)>$", argument)
result = None
guild = ctx.guild
if match is None:
# not a mention
if guild:
result = discord.utils.get(guild.voice_channels, name=argument)
else:
def check(c):
return isinstance(c, discord.VoiceChannel) and c.name == argument
result = discord.utils.find(check, bot.get_all_channels())
else:
channel_id = int(match.group(1))
if guild:
result = guild.get_channel(channel_id)
else:
result = _get_from_guilds(bot, "get_channel", channel_id)
if not isinstance(result, discord.VoiceChannel):
raise BadArgument('Channel "{}" not found.'.format(argument))
return result
class CategoryChannelConverter(IDConverter):
"""Converts to a :class:`CategoryChannel`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name
"""
async def convert(self, ctx, argument):
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r"<#([0-9]+)>$", argument)
result = None
guild = ctx.guild
if match is None:
# not a mention
if guild:
result = discord.utils.get(guild.categories, name=argument)
else:
def check(c):
return isinstance(c, discord.CategoryChannel) and c.name == argument
result = discord.utils.find(check, bot.get_all_channels())
else:
channel_id = int(match.group(1))
if guild:
result = guild.get_channel(channel_id)
else:
result = _get_from_guilds(bot, "get_channel", channel_id)
if not isinstance(result, discord.CategoryChannel):
raise BadArgument('Channel "{}" not found.'.format(argument))
return result
class ColourConverter(Converter):
"""Converts to a :class:`Colour`.
The following formats are accepted:
- ``0x<hex>``
- ``#<hex>``
- ``0x#<hex>``
- Any of the ``classmethod`` in :class:`Colour`
- The ``_`` in the name can be optionally replaced with spaces.
"""
async def convert(self, ctx, argument):
arg = argument.replace("0x", "").lower()
if arg[0] == "#":
arg = arg[1:]
try:
value = int(arg, base=16)
return discord.Colour(value=value)
except ValueError:
method = getattr(discord.Colour, arg.replace(" ", "_"), None)
if method is None or not inspect.ismethod(method):
raise BadArgument('Colour "{}" is invalid.'.format(arg))
return method()
class RoleConverter(IDConverter):
"""Converts to a :class:`Role`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name
"""
async def convert(self, ctx, argument):
guild = ctx.guild
if not guild:
raise NoPrivateMessage()
match = self._get_id_match(argument) or re.match(r"<@&([0-9]+)>$", argument)
if match:
result = guild.get_role(int(match.group(1)))
else:
result = discord.utils.get(guild._roles.values(), name=argument)
if result is None:
raise BadArgument('Role "{}" not found.'.format(argument))
return result
class GameConverter(Converter):
"""Converts to :class:`Game`."""
async def convert(self, ctx, argument):
return discord.Game(name=argument)
class InviteConverter(Converter):
"""Converts to a :class:`Invite`.
This is done via an HTTP request using :meth:`.Bot.get_invite`.
"""
async def convert(self, ctx, argument):
try:
invite = await ctx.bot.get_invite(argument)
return invite
except Exception as exc:
raise BadArgument("Invite is invalid or expired") from exc
class EmojiConverter(IDConverter):
"""Converts to a :class:`Emoji`.
All lookups are done for the local guild first, if available. If that lookup
fails, then it checks the client's global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by extracting ID from the emoji.
3. Lookup by name
"""
async def convert(self, ctx, argument):
match = self._get_id_match(argument) or re.match(
r"<a?:[a-zA-Z0-9\_]+:([0-9]+)>$", argument
)
result = None
bot = ctx.bot
guild = ctx.guild
if match is None:
# Try to get the emoji by name. Try local guild first.
if guild:
result = discord.utils.get(guild.emojis, name=argument)
if result is None:
result = discord.utils.get(bot.emojis, name=argument)
else:
emoji_id = int(match.group(1))
# Try to look up emoji by id.
if guild:
result = discord.utils.get(guild.emojis, id=emoji_id)
if result is None:
result = discord.utils.get(bot.emojis, id=emoji_id)
if result is None:
raise BadArgument('Emoji "{}" not found.'.format(argument))
return result
class PartialEmojiConverter(Converter):
"""Converts to a :class:`PartialEmoji`.
This is done by extracting the animated flag, name and ID from the emoji.
"""
async def convert(self, ctx, argument):
match = re.match(r"<(a?):([a-zA-Z0-9\_]+):([0-9]+)>$", argument)
if match:
emoji_animated = bool(match.group(1))
emoji_name = match.group(2)
emoji_id = int(match.group(3))
return discord.PartialEmoji(animated=emoji_animated, name=emoji_name, id=emoji_id)
raise BadArgument('Couldn\'t convert "{}" to PartialEmoji.'.format(argument))
class clean_content(Converter):
"""Converts the argument to mention scrubbed version of
said content.
This behaves similarly to :attr:`.Message.clean_content`.
Attributes
------------
fix_channel_mentions: :obj:`bool`
Whether to clean channel mentions.
use_nicknames: :obj:`bool`
Whether to use nicknames when transforming mentions.
escape_markdown: :obj:`bool`
Whether to also escape special markdown characters.
"""
def __init__(self, *, fix_channel_mentions=False, use_nicknames=True, escape_markdown=False):
self.fix_channel_mentions = fix_channel_mentions
self.use_nicknames = use_nicknames
self.escape_markdown = escape_markdown
async def convert(self, ctx, argument):
message = ctx.message
transformations = {}
if self.fix_channel_mentions and ctx.guild:
def resolve_channel(id, *, _get=ctx.guild.get_channel):
ch = _get(id)
return ("<#%s>" % id), ("#" + ch.name if ch else "#deleted-channel")
transformations.update(
resolve_channel(channel) for channel in message.raw_channel_mentions
)
if self.use_nicknames and ctx.guild:
def resolve_member(id, *, _get=ctx.guild.get_member):
m = _get(id)
return "@" + m.display_name if m else "@deleted-user"
else:
def resolve_member(id, *, _get=ctx.bot.get_user):
m = _get(id)
return "@" + m.name if m else "@deleted-user"
transformations.update(
("<@%s>" % member_id, resolve_member(member_id)) for member_id in message.raw_mentions
)
transformations.update(
("<@!%s>" % member_id, resolve_member(member_id)) for member_id in message.raw_mentions
)
if ctx.guild:
def resolve_role(_id, *, _find=ctx.guild.get_role):
r = _find(_id)
return "@" + r.name if r else "@deleted-role"
transformations.update(
("<@&%s>" % role_id, resolve_role(role_id))
for role_id in message.raw_role_mentions
)
def repl(obj):
return transformations.get(obj.group(0), "")
pattern = re.compile("|".join(transformations.keys()))
result = pattern.sub(repl, argument)
if self.escape_markdown:
transformations = {re.escape(c): "\\" + c for c in ("*", "`", "_", "~", "\\", "||")}
def replace(obj):
return transformations.get(re.escape(obj.group(0)), "")
pattern = re.compile("|".join(transformations.keys()))
result = pattern.sub(replace, result)
# Completely ensure no mentions escape:
return re.sub(r"@(everyone|here|[!&]?[0-9]{17,21})", "@\u200b\\1", result)
class _Greedy:
__slots__ = ("converter",)
def __init__(self, *, converter=None):
self.converter = converter
def __getitem__(self, params):
if not isinstance(params, tuple):
params = (params,)
if len(params) != 1:
raise TypeError("Greedy[...] only takes a single argument")
converter = params[0]
if not inspect.isclass(converter):
raise TypeError("Greedy[...] expects a type.")
if converter is str or converter is type(None) or converter is _Greedy:
raise TypeError("Greedy[%s] is invalid." % converter.__name__)
return self.__class__(converter=converter)
Greedy = _Greedy()

View File

@@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import enum
import time
__all__ = ["BucketType", "Cooldown", "CooldownMapping"]
class BucketType(enum.Enum):
default = 0
user = 1
guild = 2
channel = 3
member = 4
category = 5
class Cooldown:
__slots__ = ("rate", "per", "type", "_window", "_tokens", "_last")
def __init__(self, rate, per, type):
self.rate = int(rate)
self.per = float(per)
self.type = type
self._window = 0.0
self._tokens = self.rate
self._last = 0.0
if not isinstance(self.type, BucketType):
raise TypeError("Cooldown type must be a BucketType")
def get_tokens(self, current=None):
if not current:
current = time.time()
tokens = self._tokens
if current > self._window + self.per:
tokens = self.rate
return tokens
def update_rate_limit(self):
current = time.time()
self._last = current
self._tokens = self.get_tokens(current)
# first token used means that we start a new rate limit window
if self._tokens == self.rate:
self._window = current
# check if we are rate limited
if self._tokens == 0:
return self.per - (current - self._window)
# we're not so decrement our tokens
self._tokens -= 1
# see if we got rate limited due to this token change, and if
# so update the window to point to our current time frame
if self._tokens == 0:
self._window = current
def reset(self):
self._tokens = self.rate
self._last = 0.0
def copy(self):
return Cooldown(self.rate, self.per, self.type)
def __repr__(self):
return "<Cooldown rate: {0.rate} per: {0.per} window: {0._window} tokens: {0._tokens}>".format(
self
)
class CooldownMapping:
def __init__(self, original):
self._cache = {}
self._cooldown = original
@property
def valid(self):
return self._cooldown is not None
@classmethod
def from_cooldown(cls, rate, per, type):
return cls(Cooldown(rate, per, type))
def _bucket_key(self, msg):
bucket_type = self._cooldown.type
if bucket_type is BucketType.user:
return msg.author.id
elif bucket_type is BucketType.guild:
return (msg.guild or msg.author).id
elif bucket_type is BucketType.channel:
return msg.channel.id
elif bucket_type is BucketType.member:
return ((msg.guild and msg.guild.id), msg.author.id)
elif bucket_type is BucketType.category:
return (msg.channel.category or msg.channel).id
def _verify_cache_integrity(self):
# we want to delete all cache objects that haven't been used
# in a cooldown window. e.g. if we have a command that has a
# cooldown of 60s and it has not been used in 60s then that key should be deleted
current = time.time()
dead_keys = [k for k, v in self._cache.items() if current > v._last + v.per]
for k in dead_keys:
del self._cache[k]
def get_bucket(self, message):
if self._cooldown.type is BucketType.default:
return self._cooldown
self._verify_cache_integrity()
key = self._bucket_key(message)
if key not in self._cache:
bucket = self._cooldown.copy()
self._cache[key] = bucket
else:
bucket = self._cache[key]
return bucket

1513
discord/ext/commands/core.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,279 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from discord.errors import DiscordException
__all__ = [
"CommandError",
"MissingRequiredArgument",
"BadArgument",
"NoPrivateMessage",
"CheckFailure",
"CommandNotFound",
"DisabledCommand",
"CommandInvokeError",
"TooManyArguments",
"UserInputError",
"CommandOnCooldown",
"NotOwner",
"MissingPermissions",
"BotMissingPermissions",
"ConversionError",
"BadUnionArgument",
]
class CommandError(DiscordException):
r"""The base exception type for all command related errors.
This inherits from :exc:`discord.DiscordException`.
This exception and exceptions derived from it are handled
in a special way as they are caught and passed into a special event
from :class:`.Bot`\, :func:`on_command_error`.
"""
def __init__(self, message=None, *args):
if message is not None:
# clean-up @everyone and @here mentions
m = message.replace("@everyone", "@\u200beveryone").replace("@here", "@\u200bhere")
super().__init__(m, *args)
else:
super().__init__(*args)
class ConversionError(CommandError):
"""Exception raised when a Converter class raises non-CommandError.
This inherits from :exc:`.CommandError`.
Attributes
----------
converter: :class:`discord.ext.commands.Converter`
The converter that failed.
original
The original exception that was raised. You can also get this via
the ``__cause__`` attribute.
"""
def __init__(self, converter, original):
self.converter = converter
self.original = original
class UserInputError(CommandError):
"""The base exception type for errors that involve errors
regarding user input.
This inherits from :exc:`.CommandError`.
"""
pass
class CommandNotFound(CommandError):
"""Exception raised when a command is attempted to be invoked
but no command under that name is found.
This is not raised for invalid subcommands, rather just the
initial main command that is attempted to be invoked.
"""
pass
class MissingRequiredArgument(UserInputError):
"""Exception raised when parsing a command and a parameter
that is required is not encountered.
Attributes
-----------
param: :class:`inspect.Parameter`
The argument that is missing.
"""
def __init__(self, param):
self.param = param
super().__init__("{0.name} is a required argument that is missing.".format(param))
class TooManyArguments(UserInputError):
"""Exception raised when the command was passed too many arguments and its
:attr:`.Command.ignore_extra` attribute was not set to ``True``.
"""
pass
class BadArgument(UserInputError):
"""Exception raised when a parsing or conversion failure is encountered
on an argument to pass into a command.
"""
pass
class CheckFailure(CommandError):
"""Exception raised when the predicates in :attr:`.Command.checks` have failed."""
pass
class NoPrivateMessage(CheckFailure):
"""Exception raised when an operation does not work in private message
contexts.
"""
pass
class NotOwner(CheckFailure):
"""Exception raised when the message author is not the owner of the bot."""
pass
class DisabledCommand(CommandError):
"""Exception raised when the command being invoked is disabled."""
pass
class CommandInvokeError(CommandError):
"""Exception raised when the command being invoked raised an exception.
Attributes
-----------
original
The original exception that was raised. You can also get this via
the ``__cause__`` attribute.
"""
def __init__(self, e):
self.original = e
super().__init__("Command raised an exception: {0.__class__.__name__}: {0}".format(e))
class CommandOnCooldown(CommandError):
"""Exception raised when the command being invoked is on cooldown.
Attributes
-----------
cooldown: Cooldown
A class with attributes ``rate``, ``per``, and ``type`` similar to
the :func:`.cooldown` decorator.
retry_after: :class:`float`
The amount of seconds to wait before you can retry again.
"""
def __init__(self, cooldown, retry_after):
self.cooldown = cooldown
self.retry_after = retry_after
super().__init__("You are on cooldown. Try again in {:.2f}s".format(retry_after))
class MissingPermissions(CheckFailure):
"""Exception raised when the command invoker lacks permissions to run
command.
Attributes
-----------
missing_perms: :class:`list`
The required permissions that are missing.
"""
def __init__(self, missing_perms, *args):
self.missing_perms = missing_perms
missing = [
perm.replace("_", " ").replace("guild", "server").title() for perm in missing_perms
]
if len(missing) > 2:
fmt = "{}, and {}".format(", ".join(missing[:-1]), missing[-1])
else:
fmt = " and ".join(missing)
message = "You are missing {} permission(s) to run command.".format(fmt)
super().__init__(message, *args)
class BotMissingPermissions(CheckFailure):
"""Exception raised when the bot lacks permissions to run command.
Attributes
-----------
missing_perms: :class:`list`
The required permissions that are missing.
"""
def __init__(self, missing_perms, *args):
self.missing_perms = missing_perms
missing = [
perm.replace("_", " ").replace("guild", "server").title() for perm in missing_perms
]
if len(missing) > 2:
fmt = "{}, and {}".format(", ".join(missing[:-1]), missing[-1])
else:
fmt = " and ".join(missing)
message = "Bot requires {} permission(s) to run command.".format(fmt)
super().__init__(message, *args)
class BadUnionArgument(UserInputError):
"""Exception raised when a :class:`typing.Union` converter fails for all
its associated types.
Attributes
-----------
param: :class:`inspect.Parameter`
The parameter that failed being converted.
converters: Tuple[Type, ...]
A tuple of converters attempted in conversion, in order of failure.
errors: List[:class:`CommandError`]
A list of errors that were caught from failing the conversion.
"""
def __init__(self, param, converters, errors):
self.param = param
self.converters = converters
self.errors = errors
def _get_name(x):
try:
return x.__name__
except AttributeError:
return x.__class__.__name__
to_string = [_get_name(x) for x in converters]
if len(to_string) > 2:
fmt = "{}, or {}".format(", ".join(to_string[:-1]), to_string[-1])
else:
fmt = " or ".join(to_string)
super().__init__('Could not convert "{0.name}" into {1}.'.format(param, fmt))

View File

@@ -0,0 +1,370 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import itertools
import inspect
import discord.utils
from .core import GroupMixin, Command
from .errors import CommandError
# from discord.iterators import _FilteredAsyncIterator
# help -> shows info of bot on top/bottom and lists subcommands
# help command -> shows detailed info of command
# help command <subcommand chain> -> same as above
# <description>
# <command signature with aliases>
# <long doc>
# Cog:
# <command> <shortdoc>
# <command> <shortdoc>
# Other Cog:
# <command> <shortdoc>
# No Category:
# <command> <shortdoc>
# Type <prefix>help command for more info on a command.
# You can also type <prefix>help category for more info on a category.
class Paginator:
"""A class that aids in paginating code blocks for Discord messages.
Attributes
-----------
prefix: :class:`str`
The prefix inserted to every page. e.g. three backticks.
suffix: :class:`str`
The suffix appended at the end of every page. e.g. three backticks.
max_size: :class:`int`
The maximum amount of codepoints allowed in a page.
"""
def __init__(self, prefix="```", suffix="```", max_size=2000):
self.prefix = prefix
self.suffix = suffix
self.max_size = max_size - len(suffix)
self._current_page = [prefix]
self._count = len(prefix) + 1 # prefix + newline
self._pages = []
def add_line(self, line="", *, empty=False):
"""Adds a line to the current page.
If the line exceeds the :attr:`max_size` then an exception
is raised.
Parameters
-----------
line: str
The line to add.
empty: bool
Indicates if another empty line should be added.
Raises
------
RuntimeError
The line was too big for the current :attr:`max_size`.
"""
if len(line) > self.max_size - len(self.prefix) - 2:
raise RuntimeError(
"Line exceeds maximum page size %s" % (self.max_size - len(self.prefix) - 2)
)
if self._count + len(line) + 1 > self.max_size:
self.close_page()
self._count += len(line) + 1
self._current_page.append(line)
if empty:
self._current_page.append("")
self._count += 1
def close_page(self):
"""Prematurely terminate a page."""
self._current_page.append(self.suffix)
self._pages.append("\n".join(self._current_page))
self._current_page = [self.prefix]
self._count = len(self.prefix) + 1 # prefix + newline
@property
def pages(self):
"""Returns the rendered list of pages."""
# we have more than just the prefix in our current page
if len(self._current_page) > 1:
self.close_page()
return self._pages
def __repr__(self):
fmt = "<Paginator prefix: {0.prefix} suffix: {0.suffix} max_size: {0.max_size} count: {0._count}>"
return fmt.format(self)
class HelpFormatter:
"""The default base implementation that handles formatting of the help
command.
To override the behaviour of the formatter, :meth:`~.HelpFormatter.format`
should be overridden. A number of utility functions are provided for use
inside that method.
Attributes
-----------
show_hidden: :class:`bool`
Dictates if hidden commands should be shown in the output.
Defaults to ``False``.
show_check_failure: :class:`bool`
Dictates if commands that have their :attr:`.Command.checks` failed
shown. Defaults to ``False``.
width: :class:`int`
The maximum number of characters that fit in a line.
Defaults to 80.
"""
def __init__(self, show_hidden=False, show_check_failure=False, width=80):
self.width = width
self.show_hidden = show_hidden
self.show_check_failure = show_check_failure
def has_subcommands(self):
""":class:`bool`: Specifies if the command has subcommands."""
return isinstance(self.command, GroupMixin)
def is_bot(self):
""":class:`bool`: Specifies if the command being formatted is the bot itself."""
return self.command is self.context.bot
def is_cog(self):
""":class:`bool`: Specifies if the command being formatted is actually a cog."""
return not self.is_bot() and not isinstance(self.command, Command)
def shorten(self, text):
"""Shortens text to fit into the :attr:`width`."""
if len(text) > self.width:
return text[: self.width - 3] + "..."
return text
@property
def max_name_size(self):
""":class:`int`: Returns the largest name length of a command or if it has subcommands
the largest subcommand name."""
try:
commands = (
self.command.all_commands if not self.is_cog() else self.context.bot.all_commands
)
if commands:
return max(
map(
lambda c: discord.utils._string_width(c.name)
if self.show_hidden or not c.hidden
else 0,
commands.values(),
)
)
return 0
except AttributeError:
return len(self.command.name)
@property
def clean_prefix(self):
"""The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``."""
user = self.context.guild.me if self.context.guild else self.context.bot.user
# this breaks if the prefix mention is not the bot itself but I
# consider this to be an *incredibly* strange use case. I'd rather go
# for this common use case rather than waste performance for the
# odd one.
return self.context.prefix.replace(user.mention, "@" + user.display_name)
def get_command_signature(self):
"""Retrieves the signature portion of the help page."""
prefix = self.clean_prefix
cmd = self.command
return prefix + cmd.signature
def get_ending_note(self):
command_name = self.context.invoked_with
return (
"Type {0}{1} command for more info on a command.\n"
"You can also type {0}{1} category for more info on a category.".format(
self.clean_prefix, command_name
)
)
async def filter_command_list(self):
"""Returns a filtered list of commands based on the two attributes
provided, :attr:`show_check_failure` and :attr:`show_hidden`.
Also filters based on if :meth:`~.HelpFormatter.is_cog` is valid.
Returns
--------
iterable
An iterable with the filter being applied. The resulting value is
a (key, value) :class:`tuple` of the command name and the command itself.
"""
def sane_no_suspension_point_predicate(tup):
cmd = tup[1]
if self.is_cog():
# filter commands that don't exist to this cog.
if cmd.instance is not self.command:
return False
if cmd.hidden and not self.show_hidden:
return False
return True
async def predicate(tup):
if sane_no_suspension_point_predicate(tup) is False:
return False
cmd = tup[1]
try:
return await cmd.can_run(self.context)
except CommandError:
return False
iterator = (
self.command.all_commands.items()
if not self.is_cog()
else self.context.bot.all_commands.items()
)
if self.show_check_failure:
return filter(sane_no_suspension_point_predicate, iterator)
# Gotta run every check and verify it
ret = []
for elem in iterator:
valid = await predicate(elem)
if valid:
ret.append(elem)
return ret
def _add_subcommands_to_page(self, max_width, commands):
for name, command in commands:
if name in command.aliases:
# skip aliases
continue
width_gap = discord.utils._string_width(name) - len(name)
entry = " {0:<{width}} {1}".format(
name, command.short_doc, width=max_width - width_gap
)
shortened = self.shorten(entry)
self._paginator.add_line(shortened)
async def format_help_for(self, context, command_or_bot):
"""Formats the help page and handles the actual heavy lifting of how
the help command looks like. To change the behaviour, override the
:meth:`~.HelpFormatter.format` method.
Parameters
-----------
context: :class:`.Context`
The context of the invoked help command.
command_or_bot: :class:`.Command` or :class:`.Bot`
The bot or command that we are getting the help of.
Returns
--------
list
A paginated output of the help command.
"""
self.context = context
self.command = command_or_bot
return await self.format()
async def format(self):
"""Handles the actual behaviour involved with formatting.
To change the behaviour, this method should be overridden.
Returns
--------
list
A paginated output of the help command.
"""
self._paginator = Paginator()
# we need a padding of ~80 or so
description = (
self.command.description if not self.is_cog() else inspect.getdoc(self.command)
)
if description:
# <description> portion
self._paginator.add_line(description, empty=True)
if isinstance(self.command, Command):
# <signature portion>
signature = self.get_command_signature()
self._paginator.add_line(signature, empty=True)
# <long doc> section
if self.command.help:
self._paginator.add_line(self.command.help, empty=True)
# end it here if it's just a regular command
if not self.has_subcommands():
self._paginator.close_page()
return self._paginator.pages
max_width = self.max_name_size
def category(tup):
cog = tup[1].cog_name
# we insert the zero width space there to give it approximate
# last place sorting position.
return cog + ":" if cog is not None else "\u200bNo Category:"
filtered = await self.filter_command_list()
if self.is_bot():
data = sorted(filtered, key=category)
for category, commands in itertools.groupby(data, key=category):
# there simply is no prettier way of doing this.
commands = sorted(commands)
if len(commands) > 0:
self._paginator.add_line(category)
self._add_subcommands_to_page(max_width, commands)
else:
filtered = sorted(filtered)
if filtered:
self._paginator.add_line("Commands:")
self._add_subcommands_to_page(max_width, filtered)
# add the ending note
self._paginator.add_line()
ending_note = self.get_ending_note()
self._paginator.add_line(ending_note)
return self._paginator.pages

View File

@@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from .errors import BadArgument
class StringView:
def __init__(self, buffer):
self.index = 0
self.buffer = buffer
self.end = len(buffer)
self.previous = 0
@property
def current(self):
return None if self.eof else self.buffer[self.index]
@property
def eof(self):
return self.index >= self.end
def undo(self):
self.index = self.previous
def skip_ws(self):
pos = 0
while not self.eof:
try:
current = self.buffer[self.index + pos]
if not current.isspace():
break
pos += 1
except IndexError:
break
self.previous = self.index
self.index += pos
return self.previous != self.index
def skip_string(self, string):
strlen = len(string)
if self.buffer[self.index : self.index + strlen] == string:
self.previous = self.index
self.index += strlen
return True
return False
def read_rest(self):
result = self.buffer[self.index :]
self.previous = self.index
self.index = self.end
return result
def read(self, n):
result = self.buffer[self.index : self.index + n]
self.previous = self.index
self.index += n
return result
def get(self):
try:
result = self.buffer[self.index + 1]
except IndexError:
result = None
self.previous = self.index
self.index += 1
return result
def get_word(self):
pos = 0
while not self.eof:
try:
current = self.buffer[self.index + pos]
if current.isspace():
break
pos += 1
except IndexError:
break
self.previous = self.index
result = self.buffer[self.index : self.index + pos]
self.index += pos
return result
def __repr__(self):
return "<StringView pos: {0.index} prev: {0.previous} end: {0.end} eof: {0.eof}>".format(
self
)
# Parser
# map from opening quotes to closing quotes
_quotes = {
'"': '"',
"": "",
"": "",
"": "",
"": "",
"": "",
"": "",
"": "",
"": "",
"": "",
"": "",
"": "",
"": "",
"«": "»",
"": "",
"": "",
"": "",
}
_all_quotes = set(_quotes.keys()) | set(_quotes.values())
def quoted_word(view):
current = view.current
if current is None:
return None
close_quote = _quotes.get(current)
is_quoted = bool(close_quote)
if is_quoted:
result = []
_escaped_quotes = (current, close_quote)
else:
result = [current]
_escaped_quotes = _all_quotes
while not view.eof:
current = view.get()
if not current:
if is_quoted:
# unexpected EOF
raise BadArgument("Expected closing {}.".format(close_quote))
return "".join(result)
# currently we accept strings in the format of "hello world"
# to embed a quote inside the string you must escape it: "a \"world\""
if current == "\\":
next_char = view.get()
if not next_char:
# string ends with \ and no character after it
if is_quoted:
# if we're quoted then we're expecting a closing quote
raise BadArgument("Expected closing {}.".format(close_quote))
# if we aren't then we just let it through
return "".join(result)
if next_char in _escaped_quotes:
# escaped quote
result.append(next_char)
else:
# different escape character, ignore it
view.undo()
result.append(current)
continue
if not is_quoted and current in _all_quotes:
# we aren't quoted
raise BadArgument("Unexpected quote mark in non-quoted string")
# closing quote
if is_quoted and current == close_quote:
next_char = view.get()
valid_eof = not next_char or next_char.isspace()
if not valid_eof:
raise BadArgument("Expected space after closing quotation")
# we're quoted so it's okay
return "".join(result)
if current.isspace() and not is_quoted:
# end of word found
return "".join(result)
result.append(current)

81
discord/file.py Normal file
View File

@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import os.path
class File:
"""A parameter object used for :meth:`abc.Messageable.send`
for sending file objects.
Attributes
-----------
fp: Union[:class:`str`, BinaryIO]
A file-like object opened in binary mode and read mode
or a filename representing a file in the hard drive to
open.
.. note::
If the file-like object passed is opened via ``open`` then the
modes 'rb' should be used.
To pass binary data, consider usage of ``io.BytesIO``.
filename: Optional[:class:`str`]
The filename to display when uploading to Discord.
If this is not given then it defaults to ``fp.name`` or if ``fp`` is
a string then the ``filename`` will default to the string given.
spoiler: :class:`bool`
Whether the attachment is a spoiler.
"""
__slots__ = ("fp", "filename", "_true_fp")
def __init__(self, fp, filename=None, *, spoiler=False):
self.fp = fp
self._true_fp = None
if filename is None:
if isinstance(fp, str):
_, self.filename = os.path.split(fp)
else:
self.filename = getattr(fp, "name", None)
else:
self.filename = filename
if spoiler and not self.filename.startswith("SPOILER_"):
self.filename = "SPOILER_" + self.filename
def open_file(self):
fp = self.fp
if isinstance(fp, str):
self._true_fp = fp = open(fp, "rb")
return fp
def close(self):
if self._true_fp:
self._true_fp.close()

735
discord/gateway.py Normal file
View File

@@ -0,0 +1,735 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import asyncio
from collections import namedtuple
import concurrent.futures
import json
import logging
import struct
import sys
import time
import threading
import zlib
import websockets
from . import utils
from .activity import _ActivityTag
from .enums import SpeakingState
from .errors import ConnectionClosed, InvalidArgument
log = logging.getLogger(__name__)
__all__ = [
"DiscordWebSocket",
"KeepAliveHandler",
"VoiceKeepAliveHandler",
"DiscordVoiceWebSocket",
"ResumeWebSocket",
]
class ResumeWebSocket(Exception):
"""Signals to initialise via RESUME opcode instead of IDENTIFY."""
def __init__(self, shard_id):
self.shard_id = shard_id
EventListener = namedtuple("EventListener", "predicate event result future")
class KeepAliveHandler(threading.Thread):
def __init__(self, *args, **kwargs):
ws = kwargs.pop("ws", None)
interval = kwargs.pop("interval", None)
shard_id = kwargs.pop("shard_id", None)
threading.Thread.__init__(self, *args, **kwargs)
self.ws = ws
self.interval = interval
self.daemon = True
self.shard_id = shard_id
self.msg = "Keeping websocket alive with sequence %s."
self.block_msg = "Heartbeat blocked for more than %s seconds."
self.behind_msg = "Can't keep up, websocket is %.1fs behind."
self._stop_ev = threading.Event()
self._last_ack = time.perf_counter()
self._last_send = time.perf_counter()
self.latency = float("inf")
self.heartbeat_timeout = ws._max_heartbeat_timeout
def run(self):
while not self._stop_ev.wait(self.interval):
if self._last_ack + self.heartbeat_timeout < time.perf_counter():
log.warning(
"Shard ID %s has stopped responding to the gateway. Closing and restarting.",
self.shard_id,
)
coro = self.ws.close(4000)
f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop)
try:
f.result()
except Exception:
pass
finally:
self.stop()
return
data = self.get_payload()
log.debug(self.msg, data["d"])
coro = self.ws.send_as_json(data)
f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop)
try:
# block until sending is complete
total = 0
while True:
try:
f.result(5)
break
except concurrent.futures.TimeoutError:
total += 5
log.warning(self.block_msg, total)
except Exception:
self.stop()
else:
self._last_send = time.perf_counter()
def get_payload(self):
return {"op": self.ws.HEARTBEAT, "d": self.ws.sequence}
def stop(self):
self._stop_ev.set()
def ack(self):
ack_time = time.perf_counter()
self._last_ack = ack_time
self.latency = ack_time - self._last_send
if self.latency > 10:
log.warning(self.behind_msg, self.latency)
class VoiceKeepAliveHandler(KeepAliveHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.msg = "Keeping voice websocket alive with timestamp %s."
self.block_msg = "Voice heartbeat blocked for more than %s seconds"
self.behind_msg = "Can't keep up, voice websocket is %.1fs behind"
def get_payload(self):
return {"op": self.ws.HEARTBEAT, "d": int(time.time() * 1000)}
class DiscordWebSocket(websockets.client.WebSocketClientProtocol):
"""Implements a WebSocket for Discord's gateway v6.
This is created through :func:`create_main_websocket`. Library
users should never create this manually.
Attributes
-----------
DISPATCH
Receive only. Denotes an event to be sent to Discord, such as READY.
HEARTBEAT
When received tells Discord to keep the connection alive.
When sent asks if your connection is currently alive.
IDENTIFY
Send only. Starts a new session.
PRESENCE
Send only. Updates your presence.
VOICE_STATE
Send only. Starts a new connection to a voice guild.
VOICE_PING
Send only. Checks ping time to a voice guild, do not use.
RESUME
Send only. Resumes an existing connection.
RECONNECT
Receive only. Tells the client to reconnect to a new gateway.
REQUEST_MEMBERS
Send only. Asks for the full member list of a guild.
INVALIDATE_SESSION
Receive only. Tells the client to optionally invalidate the session
and IDENTIFY again.
HELLO
Receive only. Tells the client the heartbeat interval.
HEARTBEAT_ACK
Receive only. Confirms receiving of a heartbeat. Not having it implies
a connection issue.
GUILD_SYNC
Send only. Requests a guild sync.
gateway
The gateway we are currently connected to.
token
The authentication token for discord.
"""
DISPATCH = 0
HEARTBEAT = 1
IDENTIFY = 2
PRESENCE = 3
VOICE_STATE = 4
VOICE_PING = 5
RESUME = 6
RECONNECT = 7
REQUEST_MEMBERS = 8
INVALIDATE_SESSION = 9
HELLO = 10
HEARTBEAT_ACK = 11
GUILD_SYNC = 12
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.max_size = None
# an empty dispatcher to prevent crashes
self._dispatch = lambda *args: None
# generic event listeners
self._dispatch_listeners = []
# the keep alive
self._keep_alive = None
# ws related stuff
self.session_id = None
self.sequence = None
self._zlib = zlib.decompressobj()
self._buffer = bytearray()
@classmethod
async def from_client(
cls, client, *, shard_id=None, session=None, sequence=None, resume=False
):
"""Creates a main websocket for Discord from a :class:`Client`.
This is for internal use only.
"""
gateway = await client.http.get_gateway()
ws = await websockets.connect(gateway, loop=client.loop, klass=cls, compression=None)
# dynamically add attributes needed
ws.token = client.http.token
ws._connection = client._connection
ws._dispatch = client.dispatch
ws.gateway = gateway
ws.shard_id = shard_id
ws.shard_count = client._connection.shard_count
ws.session_id = session
ws.sequence = sequence
ws._max_heartbeat_timeout = client._connection.heartbeat_timeout
client._connection._update_references(ws)
log.info("Created websocket connected to %s", gateway)
# poll event for OP Hello
await ws.poll_event()
if not resume:
await ws.identify()
return ws
await ws.resume()
try:
await ws.ensure_open()
except websockets.exceptions.ConnectionClosed:
# ws got closed so let's just do a regular IDENTIFY connect.
log.info(
"RESUME failed (the websocket decided to close) for Shard ID %s. Retrying.",
shard_id,
)
return await cls.from_client(client, shard_id=shard_id)
else:
return ws
def wait_for(self, event, predicate, result=None):
"""Waits for a DISPATCH'd event that meets the predicate.
Parameters
-----------
event : str
The event name in all upper case to wait for.
predicate
A function that takes a data parameter to check for event
properties. The data parameter is the 'd' key in the JSON message.
result
A function that takes the same data parameter and executes to send
the result to the future. If None, returns the data.
Returns
--------
asyncio.Future
A future to wait for.
"""
future = self.loop.create_future()
entry = EventListener(event=event, predicate=predicate, result=result, future=future)
self._dispatch_listeners.append(entry)
return future
async def identify(self):
"""Sends the IDENTIFY packet."""
payload = {
"op": self.IDENTIFY,
"d": {
"token": self.token,
"properties": {
"$os": sys.platform,
"$browser": "discord.py",
"$device": "discord.py",
"$referrer": "",
"$referring_domain": "",
},
"compress": True,
"large_threshold": 250,
"v": 3,
},
}
if not self._connection.is_bot:
payload["d"]["synced_guilds"] = []
if self.shard_id is not None and self.shard_count is not None:
payload["d"]["shard"] = [self.shard_id, self.shard_count]
state = self._connection
if state._activity is not None or state._status is not None:
payload["d"]["presence"] = {
"status": state._status,
"game": state._activity,
"since": 0,
"afk": False,
}
await self.send_as_json(payload)
log.info("Shard ID %s has sent the IDENTIFY payload.", self.shard_id)
async def resume(self):
"""Sends the RESUME packet."""
payload = {
"op": self.RESUME,
"d": {"seq": self.sequence, "session_id": self.session_id, "token": self.token},
}
await self.send_as_json(payload)
log.info("Shard ID %s has sent the RESUME payload.", self.shard_id)
async def received_message(self, msg):
self._dispatch("socket_raw_receive", msg)
if type(msg) is bytes:
self._buffer.extend(msg)
if len(msg) >= 4:
if msg[-4:] == b"\x00\x00\xff\xff":
msg = self._zlib.decompress(self._buffer)
msg = msg.decode("utf-8")
self._buffer = bytearray()
else:
return
else:
return
msg = json.loads(msg)
log.debug("For Shard ID %s: WebSocket Event: %s", self.shard_id, msg)
self._dispatch("socket_response", msg)
op = msg.get("op")
data = msg.get("d")
seq = msg.get("s")
if seq is not None:
self.sequence = seq
if op != self.DISPATCH:
if op == self.RECONNECT:
# "reconnect" can only be handled by the Client
# so we terminate our connection and raise an
# internal exception signalling to reconnect.
log.info("Received RECONNECT opcode.")
await self.close()
raise ResumeWebSocket(self.shard_id)
if op == self.HEARTBEAT_ACK:
self._keep_alive.ack()
return
if op == self.HEARTBEAT:
beat = self._keep_alive.get_payload()
await self.send_as_json(beat)
return
if op == self.HELLO:
interval = data["heartbeat_interval"] / 1000.0
self._keep_alive = KeepAliveHandler(
ws=self, interval=interval, shard_id=self.shard_id
)
# send a heartbeat immediately
await self.send_as_json(self._keep_alive.get_payload())
self._keep_alive.start()
return
if op == self.INVALIDATE_SESSION:
if data is True:
await asyncio.sleep(5.0, loop=self.loop)
await self.close()
raise ResumeWebSocket(self.shard_id)
self.sequence = None
self.session_id = None
log.info("Shard ID %s session has been invalidated.", self.shard_id)
await self.identify()
return
log.warning("Unknown OP code %s.", op)
return
event = msg.get("t")
if event == "READY":
self._trace = trace = data.get("_trace", [])
self.sequence = msg["s"]
self.session_id = data["session_id"]
log.info(
"Shard ID %s has connected to Gateway: %s (Session ID: %s).",
self.shard_id,
", ".join(trace),
self.session_id,
)
elif event == "RESUMED":
self._trace = trace = data.get("_trace", [])
log.info(
"Shard ID %s has successfully RESUMED session %s under trace %s.",
self.shard_id,
self.session_id,
", ".join(trace),
)
parser = "parse_" + event.lower()
try:
func = getattr(self._connection, parser)
except AttributeError:
log.warning("Unknown event %s.", event)
else:
func(data)
# remove the dispatched listeners
removed = []
for index, entry in enumerate(self._dispatch_listeners):
if entry.event != event:
continue
future = entry.future
if future.cancelled():
removed.append(index)
continue
try:
valid = entry.predicate(data)
except Exception as exc:
future.set_exception(exc)
removed.append(index)
else:
if valid:
ret = data if entry.result is None else entry.result(data)
future.set_result(ret)
removed.append(index)
for index in reversed(removed):
del self._dispatch_listeners[index]
@property
def latency(self):
""":obj:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds."""
heartbeat = self._keep_alive
return float("inf") if heartbeat is None else heartbeat.latency
def _can_handle_close(self, code):
return code not in (1000, 4004, 4010, 4011)
async def poll_event(self):
"""Polls for a DISPATCH event and handles the general gateway loop.
Raises
------
ConnectionClosed
The websocket connection was terminated for unhandled reasons.
"""
try:
msg = await self.recv()
await self.received_message(msg)
except websockets.exceptions.ConnectionClosed as exc:
if self._can_handle_close(exc.code):
log.info(
"Websocket closed with %s (%s), attempting a reconnect.", exc.code, exc.reason
)
raise ResumeWebSocket(self.shard_id) from exc
else:
log.info("Websocket closed with %s (%s), cannot reconnect.", exc.code, exc.reason)
raise ConnectionClosed(exc, shard_id=self.shard_id) from exc
async def send(self, data):
self._dispatch("socket_raw_send", data)
await super().send(data)
async def send_as_json(self, data):
try:
await self.send(utils.to_json(data))
except websockets.exceptions.ConnectionClosed as exc:
if not self._can_handle_close(exc.code):
raise ConnectionClosed(exc, shard_id=self.shard_id) from exc
async def change_presence(self, *, activity=None, status=None, afk=False, since=0.0):
if activity is not None:
if not isinstance(activity, _ActivityTag):
raise InvalidArgument("activity must be one of Game, Streaming, or Activity.")
activity = activity.to_dict()
if status == "idle":
since = int(time.time() * 1000)
payload = {
"op": self.PRESENCE,
"d": {"game": activity, "afk": afk, "since": since, "status": status},
}
sent = utils.to_json(payload)
log.debug('Sending "%s" to change status', sent)
await self.send(sent)
async def request_sync(self, guild_ids):
payload = {"op": self.GUILD_SYNC, "d": list(guild_ids)}
await self.send_as_json(payload)
async def voice_state(self, guild_id, channel_id, self_mute=False, self_deaf=False):
payload = {
"op": self.VOICE_STATE,
"d": {
"guild_id": guild_id,
"channel_id": channel_id,
"self_mute": self_mute,
"self_deaf": self_deaf,
},
}
log.debug("Updating our voice state to %s.", payload)
await self.send_as_json(payload)
async def close(self, code=1000, reason=""):
if self._keep_alive:
self._keep_alive.stop()
await super().close(code, reason)
async def close_connection(self, *args, **kwargs):
if self._keep_alive:
self._keep_alive.stop()
await super().close_connection(*args, **kwargs)
class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
"""Implements the websocket protocol for handling voice connections.
Attributes
-----------
IDENTIFY
Send only. Starts a new voice session.
SELECT_PROTOCOL
Send only. Tells discord what encryption mode and how to connect for voice.
READY
Receive only. Tells the websocket that the initial connection has completed.
HEARTBEAT
Send only. Keeps your websocket connection alive.
SESSION_DESCRIPTION
Receive only. Gives you the secret key required for voice.
SPEAKING
Send only. Notifies the client if you are currently speaking.
HEARTBEAT_ACK
Receive only. Tells you your heartbeat has been acknowledged.
RESUME
Sent only. Tells the client to resume its session.
HELLO
Receive only. Tells you that your websocket connection was acknowledged.
INVALIDATE_SESSION
Sent only. Tells you that your RESUME request has failed and to re-IDENTIFY.
CLIENT_CONNECT
Indicates a user has connected to voice.
CLIENT_DISCONNECT
Receive only. Indicates a user has disconnected from voice.
"""
IDENTIFY = 0
SELECT_PROTOCOL = 1
READY = 2
HEARTBEAT = 3
SESSION_DESCRIPTION = 4
SPEAKING = 5
HEARTBEAT_ACK = 6
RESUME = 7
HELLO = 8
INVALIDATE_SESSION = 9
CLIENT_CONNECT = 12
CLIENT_DISCONNECT = 13
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.max_size = None
self._keep_alive = None
async def send_as_json(self, data):
log.debug("Sending voice websocket frame: %s.", data)
await self.send(utils.to_json(data))
async def resume(self):
state = self._connection
payload = {
"op": self.RESUME,
"d": {
"token": state.token,
"server_id": str(state.server_id),
"session_id": state.session_id,
},
}
await self.send_as_json(payload)
async def identify(self):
state = self._connection
payload = {
"op": self.IDENTIFY,
"d": {
"server_id": str(state.server_id),
"user_id": str(state.user.id),
"session_id": state.session_id,
"token": state.token,
},
}
await self.send_as_json(payload)
@classmethod
async def from_client(cls, client, *, resume=False):
"""Creates a voice websocket for the :class:`VoiceClient`."""
gateway = "wss://" + client.endpoint + "/?v=4"
ws = await websockets.connect(gateway, loop=client.loop, klass=cls, compression=None)
ws.gateway = gateway
ws._connection = client
ws._max_heartbeat_timeout = 60.0
if resume:
await ws.resume()
else:
await ws.identify()
return ws
async def select_protocol(self, ip, port, mode):
payload = {
"op": self.SELECT_PROTOCOL,
"d": {"protocol": "udp", "data": {"address": ip, "port": port, "mode": mode}},
}
await self.send_as_json(payload)
async def client_connect(self):
payload = {"op": self.CLIENT_CONNECT, "d": {"audio_ssrc": self._connection.ssrc}}
await self.send_as_json(payload)
async def speak(self, state=SpeakingState.voice):
payload = {"op": self.SPEAKING, "d": {"speaking": int(state), "delay": 0}}
await self.send_as_json(payload)
async def received_message(self, msg):
log.debug("Voice websocket frame received: %s", msg)
op = msg["op"]
data = msg.get("d")
if op == self.READY:
await self.initial_connection(data)
elif op == self.HEARTBEAT_ACK:
self._keep_alive.ack()
elif op == self.INVALIDATE_SESSION:
log.info("Voice RESUME failed.")
await self.identify()
elif op == self.SESSION_DESCRIPTION:
self._connection.mode = data["mode"]
await self.load_secret_key(data)
elif op == self.HELLO:
interval = data["heartbeat_interval"] / 1000.0
self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=interval)
self._keep_alive.start()
async def initial_connection(self, data):
state = self._connection
state.ssrc = data["ssrc"]
state.voice_port = data["port"]
packet = bytearray(70)
struct.pack_into(">I", packet, 0, state.ssrc)
state.socket.sendto(packet, (state.endpoint_ip, state.voice_port))
recv = await self.loop.sock_recv(state.socket, 70)
log.debug("received packet in initial_connection: %s", recv)
# the ip is ascii starting at the 4th byte and ending at the first null
ip_start = 4
ip_end = recv.index(0, ip_start)
state.ip = recv[ip_start:ip_end].decode("ascii")
# the port is a little endian unsigned short in the last two bytes
# yes, this is different endianness from everything else
state.port = struct.unpack_from("<H", recv, len(recv) - 2)[0]
log.debug("detected ip: %s port: %s", state.ip, state.port)
# there *should* always be at least one supported mode (xsalsa20_poly1305)
modes = [mode for mode in data["modes"] if mode in self._connection.supported_modes]
log.debug("received supported encryption modes: %s", ", ".join(modes))
mode = modes[0]
await self.select_protocol(state.ip, state.port, mode)
log.info("selected the voice protocol for use (%s)", mode)
await self.client_connect()
async def load_secret_key(self, data):
log.info("received secret key for voice connection")
self._connection.secret_key = data.get("secret_key")
await self.speak()
await self.speak(False)
async def poll_event(self):
try:
msg = await asyncio.wait_for(self.recv(), timeout=30.0, loop=self.loop)
await self.received_message(json.loads(msg))
except websockets.exceptions.ConnectionClosed as exc:
raise ConnectionClosed(exc, shard_id=None) from exc
async def close_connection(self, *args, **kwargs):
if self._keep_alive:
self._keep_alive.stop()
await super().close_connection(*args, **kwargs)

1452
discord/guild.py Normal file

File diff suppressed because it is too large Load Diff

909
discord/http.py Normal file
View File

@@ -0,0 +1,909 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import asyncio
import json
import logging
import sys
from urllib.parse import quote as _uriquote
import weakref
import aiohttp
from .errors import HTTPException, Forbidden, NotFound, LoginFailure, GatewayNotFound
from . import __version__, utils
log = logging.getLogger(__name__)
async def json_or_text(response):
text = await response.text(encoding="utf-8")
if response.headers["content-type"] == "application/json":
return json.loads(text)
return text
class Route:
BASE = "https://discordapp.com/api/v7"
def __init__(self, method, path, **parameters):
self.path = path
self.method = method
url = self.BASE + self.path
if parameters:
self.url = url.format(
**{k: _uriquote(v) if isinstance(v, str) else v for k, v in parameters.items()}
)
else:
self.url = url
# major parameters:
self.channel_id = parameters.get("channel_id")
self.guild_id = parameters.get("guild_id")
@property
def bucket(self):
# the bucket is just method + path w/ major parameters
return "{0.method}:{0.channel_id}:{0.guild_id}:{0.path}".format(self)
class MaybeUnlock:
def __init__(self, lock):
self.lock = lock
self._unlock = True
def __enter__(self):
return self
def defer(self):
self._unlock = False
def __exit__(self, type, value, traceback):
if self._unlock:
self.lock.release()
class HTTPClient:
"""Represents an HTTP client sending HTTP requests to the Discord API."""
SUCCESS_LOG = "{method} {url} has received {text}"
REQUEST_LOG = "{method} {url} with {json} has returned {status}"
def __init__(self, connector=None, *, proxy=None, proxy_auth=None, loop=None):
self.loop = asyncio.get_event_loop() if loop is None else loop
self.connector = connector
self._session = aiohttp.ClientSession(connector=connector, loop=self.loop)
self._locks = weakref.WeakValueDictionary()
self._global_over = asyncio.Event(loop=self.loop)
self._global_over.set()
self.token = None
self.bot_token = False
self.proxy = proxy
self.proxy_auth = proxy_auth
user_agent = "DiscordBot (https://github.com/Rapptz/discord.py {0}) Python/{1[0]}.{1[1]} aiohttp/{2}"
self.user_agent = user_agent.format(__version__, sys.version_info, aiohttp.__version__)
def recreate(self):
if self._session.closed:
self._session = aiohttp.ClientSession(connector=self.connector, loop=self.loop)
async def request(self, route, *, header_bypass_delay=None, **kwargs):
bucket = route.bucket
method = route.method
url = route.url
lock = self._locks.get(bucket)
if lock is None:
lock = asyncio.Lock(loop=self.loop)
if bucket is not None:
self._locks[bucket] = lock
# header creation
headers = {"User-Agent": self.user_agent}
if self.token is not None:
headers["Authorization"] = "Bot " + self.token if self.bot_token else self.token
# some checking if it's a JSON request
if "json" in kwargs:
headers["Content-Type"] = "application/json"
kwargs["data"] = utils.to_json(kwargs.pop("json"))
try:
reason = kwargs.pop("reason")
except KeyError:
pass
else:
if reason:
headers["X-Audit-Log-Reason"] = _uriquote(reason, safe="/ ")
kwargs["headers"] = headers
# Proxy support
if self.proxy is not None:
kwargs["proxy"] = self.proxy
if self.proxy_auth is not None:
kwargs["proxy_auth"] = self.proxy_auth
if not self._global_over.is_set():
# wait until the global lock is complete
await self._global_over.wait()
await lock.acquire()
with MaybeUnlock(lock) as maybe_lock:
for tries in range(5):
async with self._session.request(method, url, **kwargs) as r:
log.debug(
"%s %s with %s has returned %s", method, url, kwargs.get("data"), r.status
)
# even errors have text involved in them so this is safe to call
data = await json_or_text(r)
# check if we have rate limit header information
remaining = r.headers.get("X-Ratelimit-Remaining")
if remaining == "0" and r.status != 429:
# we've depleted our current bucket
if header_bypass_delay is None:
delta = utils._parse_ratelimit_header(r)
else:
delta = header_bypass_delay
log.debug(
"A rate limit bucket has been exhausted (bucket: %s, retry: %s).",
bucket,
delta,
)
maybe_lock.defer()
self.loop.call_later(delta, lock.release)
# the request was successful so just return the text/json
if 300 > r.status >= 200:
log.debug("%s %s has received %s", method, url, data)
return data
# we are being rate limited
if r.status == 429:
fmt = 'We are being rate limited. Retrying in %.2f seconds. Handled under the bucket "%s"'
# sleep a bit
retry_after = data["retry_after"] / 1000.0
log.warning(fmt, retry_after, bucket)
# check if it's a global rate limit
is_global = data.get("global", False)
if is_global:
log.warning(
"Global rate limit has been hit. Retrying in %.2f seconds.",
retry_after,
)
self._global_over.clear()
await asyncio.sleep(retry_after, loop=self.loop)
log.debug("Done sleeping for the rate limit. Retrying...")
# release the global lock now that the
# global rate limit has passed
if is_global:
self._global_over.set()
log.debug("Global rate limit is now over.")
continue
# we've received a 500 or 502, unconditional retry
if r.status in {500, 502}:
await asyncio.sleep(1 + tries * 2, loop=self.loop)
continue
# the usual error cases
if r.status == 403:
raise Forbidden(r, data)
elif r.status == 404:
raise NotFound(r, data)
else:
raise HTTPException(r, data)
# We've run out of retries, raise.
raise HTTPException(r, data)
async def get_attachment(self, url):
async with self._session.get(url) as resp:
if resp.status == 200:
return await resp.read()
elif resp.status == 404:
raise NotFound(resp, "attachment not found")
elif resp.status == 403:
raise Forbidden(resp, "cannot retrieve attachment")
else:
raise HTTPException(resp, "failed to get attachment")
# state management
async def close(self):
await self._session.close()
def _token(self, token, *, bot=True):
self.token = token
self.bot_token = bot
self._ack_token = None
# login management
async def static_login(self, token, *, bot):
old_token, old_bot = self.token, self.bot_token
self._token(token, bot=bot)
try:
data = await self.request(Route("GET", "/users/@me"))
except HTTPException as exc:
self._token(old_token, bot=old_bot)
if exc.response.status == 401:
raise LoginFailure("Improper token has been passed.") from exc
raise
return data
def logout(self):
return self.request(Route("POST", "/auth/logout"))
# Group functionality
def start_group(self, user_id, recipients):
payload = {"recipients": recipients}
return self.request(
Route("POST", "/users/{user_id}/channels", user_id=user_id), json=payload
)
def leave_group(self, channel_id):
return self.request(Route("DELETE", "/channels/{channel_id}", channel_id=channel_id))
def add_group_recipient(self, channel_id, user_id):
r = Route(
"PUT",
"/channels/{channel_id}/recipients/{user_id}",
channel_id=channel_id,
user_id=user_id,
)
return self.request(r)
def remove_group_recipient(self, channel_id, user_id):
r = Route(
"DELETE",
"/channels/{channel_id}/recipients/{user_id}",
channel_id=channel_id,
user_id=user_id,
)
return self.request(r)
def edit_group(self, channel_id, **options):
valid_keys = ("name", "icon")
payload = {k: v for k, v in options.items() if k in valid_keys}
return self.request(
Route("PATCH", "/channels/{channel_id}", channel_id=channel_id), json=payload
)
def convert_group(self, channel_id):
return self.request(Route("POST", "/channels/{channel_id}/convert", channel_id=channel_id))
# Message management
def start_private_message(self, user_id):
payload = {"recipient_id": user_id}
return self.request(Route("POST", "/users/@me/channels"), json=payload)
def send_message(self, channel_id, content, *, tts=False, embed=None, nonce=None):
r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id)
payload = {}
if content:
payload["content"] = content
if tts:
payload["tts"] = True
if embed:
payload["embed"] = embed
if nonce:
payload["nonce"] = nonce
return self.request(r, json=payload)
def send_typing(self, channel_id):
return self.request(Route("POST", "/channels/{channel_id}/typing", channel_id=channel_id))
def send_files(self, channel_id, *, files, content=None, tts=False, embed=None, nonce=None):
r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id)
form = aiohttp.FormData()
payload = {"tts": tts}
if content:
payload["content"] = content
if embed:
payload["embed"] = embed
if nonce:
payload["nonce"] = nonce
form.add_field("payload_json", utils.to_json(payload))
if len(files) == 1:
fp = files[0]
form.add_field("file", fp[0], filename=fp[1], content_type="application/octet-stream")
else:
for index, (buffer, filename) in enumerate(files):
form.add_field(
"file%s" % index,
buffer,
filename=filename,
content_type="application/octet-stream",
)
return self.request(r, data=form)
async def ack_message(self, channel_id, message_id):
r = Route(
"POST",
"/channels/{channel_id}/messages/{message_id}/ack",
channel_id=channel_id,
message_id=message_id,
)
data = await self.request(r, json={"token": self._ack_token})
self._ack_token = data["token"]
def ack_guild(self, guild_id):
return self.request(Route("POST", "/guilds/{guild_id}/ack", guild_id=guild_id))
def delete_message(self, channel_id, message_id, *, reason=None):
r = Route(
"DELETE",
"/channels/{channel_id}/messages/{message_id}",
channel_id=channel_id,
message_id=message_id,
)
return self.request(r, reason=reason)
def delete_messages(self, channel_id, message_ids, *, reason=None):
r = Route("POST", "/channels/{channel_id}/messages/bulk_delete", channel_id=channel_id)
payload = {"messages": message_ids}
return self.request(r, json=payload, reason=reason)
def edit_message(self, message_id, channel_id, **fields):
r = Route(
"PATCH",
"/channels/{channel_id}/messages/{message_id}",
channel_id=channel_id,
message_id=message_id,
)
return self.request(r, json=fields)
def add_reaction(self, message_id, channel_id, emoji):
r = Route(
"PUT",
"/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me",
channel_id=channel_id,
message_id=message_id,
emoji=emoji,
)
return self.request(r, header_bypass_delay=0.25)
def remove_reaction(self, message_id, channel_id, emoji, member_id):
r = Route(
"DELETE",
"/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{member_id}",
channel_id=channel_id,
message_id=message_id,
member_id=member_id,
emoji=emoji,
)
return self.request(r, header_bypass_delay=0.25)
def remove_own_reaction(self, message_id, channel_id, emoji):
r = Route(
"DELETE",
"/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me",
channel_id=channel_id,
message_id=message_id,
emoji=emoji,
)
return self.request(r, header_bypass_delay=0.25)
def get_reaction_users(self, message_id, channel_id, emoji, limit, after=None):
r = Route(
"GET",
"/channels/{channel_id}/messages/{message_id}/reactions/{emoji}",
channel_id=channel_id,
message_id=message_id,
emoji=emoji,
)
params = {"limit": limit}
if after:
params["after"] = after
return self.request(r, params=params)
def clear_reactions(self, message_id, channel_id):
r = Route(
"DELETE",
"/channels/{channel_id}/messages/{message_id}/reactions",
channel_id=channel_id,
message_id=message_id,
)
return self.request(r)
def get_message(self, channel_id, message_id):
r = Route(
"GET",
"/channels/{channel_id}/messages/{message_id}",
channel_id=channel_id,
message_id=message_id,
)
return self.request(r)
def logs_from(self, channel_id, limit, before=None, after=None, around=None):
params = {"limit": limit}
if before:
params["before"] = before
if after:
params["after"] = after
if around:
params["around"] = around
return self.request(
Route("GET", "/channels/{channel_id}/messages", channel_id=channel_id), params=params
)
def pin_message(self, channel_id, message_id):
return self.request(
Route(
"PUT",
"/channels/{channel_id}/pins/{message_id}",
channel_id=channel_id,
message_id=message_id,
)
)
def unpin_message(self, channel_id, message_id):
return self.request(
Route(
"DELETE",
"/channels/{channel_id}/pins/{message_id}",
channel_id=channel_id,
message_id=message_id,
)
)
def pins_from(self, channel_id):
return self.request(Route("GET", "/channels/{channel_id}/pins", channel_id=channel_id))
# Member management
def kick(self, user_id, guild_id, reason=None):
r = Route(
"DELETE", "/guilds/{guild_id}/members/{user_id}", guild_id=guild_id, user_id=user_id
)
if reason:
# thanks aiohttp
r.url = "{0.url}?reason={1}".format(r, _uriquote(reason))
return self.request(r)
def ban(self, user_id, guild_id, delete_message_days=1, reason=None):
r = Route("PUT", "/guilds/{guild_id}/bans/{user_id}", guild_id=guild_id, user_id=user_id)
params = {"delete-message-days": delete_message_days}
if reason:
# thanks aiohttp
r.url = "{0.url}?reason={1}".format(r, _uriquote(reason))
return self.request(r, params=params)
def unban(self, user_id, guild_id, *, reason=None):
r = Route(
"DELETE", "/guilds/{guild_id}/bans/{user_id}", guild_id=guild_id, user_id=user_id
)
return self.request(r, reason=reason)
def guild_voice_state(self, user_id, guild_id, *, mute=None, deafen=None, reason=None):
r = Route(
"PATCH", "/guilds/{guild_id}/members/{user_id}", guild_id=guild_id, user_id=user_id
)
payload = {}
if mute is not None:
payload["mute"] = mute
if deafen is not None:
payload["deaf"] = deafen
return self.request(r, json=payload, reason=reason)
def edit_profile(self, password, username, avatar, **fields):
payload = {"password": password, "username": username, "avatar": avatar}
if "email" in fields:
payload["email"] = fields["email"]
if "new_password" in fields:
payload["new_password"] = fields["new_password"]
return self.request(Route("PATCH", "/users/@me"), json=payload)
def change_my_nickname(self, guild_id, nickname, *, reason=None):
r = Route("PATCH", "/guilds/{guild_id}/members/@me/nick", guild_id=guild_id)
payload = {"nick": nickname}
return self.request(r, json=payload, reason=reason)
def change_nickname(self, guild_id, user_id, nickname, *, reason=None):
r = Route(
"PATCH", "/guilds/{guild_id}/members/{user_id}", guild_id=guild_id, user_id=user_id
)
payload = {"nick": nickname}
return self.request(r, json=payload, reason=reason)
def edit_member(self, guild_id, user_id, *, reason=None, **fields):
r = Route(
"PATCH", "/guilds/{guild_id}/members/{user_id}", guild_id=guild_id, user_id=user_id
)
return self.request(r, json=fields, reason=reason)
# Channel management
def edit_channel(self, channel_id, *, reason=None, **options):
r = Route("PATCH", "/channels/{channel_id}", channel_id=channel_id)
valid_keys = (
"name",
"parent_id",
"topic",
"bitrate",
"nsfw",
"user_limit",
"position",
"permission_overwrites",
"rate_limit_per_user",
)
payload = {k: v for k, v in options.items() if k in valid_keys}
return self.request(r, reason=reason, json=payload)
def bulk_channel_update(self, guild_id, data, *, reason=None):
r = Route("PATCH", "/guilds/{guild_id}/channels", guild_id=guild_id)
return self.request(r, json=data, reason=reason)
def create_channel(self, guild_id, channel_type, *, reason=None, **options):
payload = {"type": channel_type}
valid_keys = (
"name",
"parent_id",
"topic",
"bitrate",
"nsfw",
"user_limit",
"position",
"permission_overwrites",
"rate_limit_per_user",
)
payload.update({k: v for k, v in options.items() if k in valid_keys and v is not None})
return self.request(
Route("POST", "/guilds/{guild_id}/channels", guild_id=guild_id),
json=payload,
reason=reason,
)
def delete_channel(self, channel_id, *, reason=None):
return self.request(
Route("DELETE", "/channels/{channel_id}", channel_id=channel_id), reason=reason
)
# Webhook management
def create_webhook(self, channel_id, *, name, avatar=None):
payload = {"name": name}
if avatar is not None:
payload["avatar"] = avatar
return self.request(
Route("POST", "/channels/{channel_id}/webhooks", channel_id=channel_id), json=payload
)
def channel_webhooks(self, channel_id):
return self.request(Route("GET", "/channels/{channel_id}/webhooks", channel_id=channel_id))
def guild_webhooks(self, guild_id):
return self.request(Route("GET", "/guilds/{guild_id}/webhooks", guild_id=guild_id))
def get_webhook(self, webhook_id):
return self.request(Route("GET", "/webhooks/{webhook_id}", webhook_id=webhook_id))
# Guild management
def leave_guild(self, guild_id):
return self.request(Route("DELETE", "/users/@me/guilds/{guild_id}", guild_id=guild_id))
def delete_guild(self, guild_id):
return self.request(Route("DELETE", "/guilds/{guild_id}", guild_id=guild_id))
def create_guild(self, name, region, icon):
payload = {"name": name, "icon": icon, "region": region}
return self.request(Route("POST", "/guilds"), json=payload)
def edit_guild(self, guild_id, *, reason=None, **fields):
valid_keys = (
"name",
"region",
"icon",
"afk_timeout",
"owner_id",
"afk_channel_id",
"splash",
"verification_level",
"system_channel_id",
"default_message_notifications",
"explicit_content_filter",
)
payload = {k: v for k, v in fields.items() if k in valid_keys}
return self.request(
Route("PATCH", "/guilds/{guild_id}", guild_id=guild_id), json=payload, reason=reason
)
def get_bans(self, guild_id):
return self.request(Route("GET", "/guilds/{guild_id}/bans", guild_id=guild_id))
def get_ban(self, user_id, guild_id):
return self.request(
Route("GET", "/guilds/{guild_id}/bans/{user_id}", guild_id=guild_id, user_id=user_id)
)
def get_vanity_code(self, guild_id):
return self.request(Route("GET", "/guilds/{guild_id}/vanity-url", guild_id=guild_id))
def change_vanity_code(self, guild_id, code, *, reason=None):
payload = {"code": code}
return self.request(
Route("PATCH", "/guilds/{guild_id}/vanity-url", guild_id=guild_id),
json=payload,
reason=reason,
)
def prune_members(self, guild_id, days, *, reason=None):
params = {"days": days}
return self.request(
Route("POST", "/guilds/{guild_id}/prune", guild_id=guild_id),
params=params,
reason=reason,
)
def estimate_pruned_members(self, guild_id, days):
params = {"days": days}
return self.request(
Route("GET", "/guilds/{guild_id}/prune", guild_id=guild_id), params=params
)
def create_custom_emoji(self, guild_id, name, image, *, roles=None, reason=None):
payload = {"name": name, "image": image, "roles": roles or []}
r = Route("POST", "/guilds/{guild_id}/emojis", guild_id=guild_id)
return self.request(r, json=payload, reason=reason)
def delete_custom_emoji(self, guild_id, emoji_id, *, reason=None):
r = Route(
"DELETE", "/guilds/{guild_id}/emojis/{emoji_id}", guild_id=guild_id, emoji_id=emoji_id
)
return self.request(r, reason=reason)
def edit_custom_emoji(self, guild_id, emoji_id, *, name, roles=None, reason=None):
payload = {"name": name, "roles": roles or []}
r = Route(
"PATCH", "/guilds/{guild_id}/emojis/{emoji_id}", guild_id=guild_id, emoji_id=emoji_id
)
return self.request(r, json=payload, reason=reason)
def get_audit_logs(
self, guild_id, limit=100, before=None, after=None, user_id=None, action_type=None
):
params = {"limit": limit}
if before:
params["before"] = before
if after:
params["after"] = after
if user_id:
params["user_id"] = user_id
if action_type:
params["action_type"] = action_type
r = Route("GET", "/guilds/{guild_id}/audit-logs", guild_id=guild_id)
return self.request(r, params=params)
# Invite management
def create_invite(self, channel_id, *, reason=None, **options):
r = Route("POST", "/channels/{channel_id}/invites", channel_id=channel_id)
payload = {
"max_age": options.get("max_age", 0),
"max_uses": options.get("max_uses", 0),
"temporary": options.get("temporary", False),
"unique": options.get("unique", True),
}
return self.request(r, reason=reason, json=payload)
def get_invite(self, invite_id):
return self.request(Route("GET", "/invite/{invite_id}", invite_id=invite_id))
def invites_from(self, guild_id):
return self.request(Route("GET", "/guilds/{guild_id}/invites", guild_id=guild_id))
def invites_from_channel(self, channel_id):
return self.request(Route("GET", "/channels/{channel_id}/invites", channel_id=channel_id))
def delete_invite(self, invite_id, *, reason=None):
return self.request(
Route("DELETE", "/invite/{invite_id}", invite_id=invite_id), reason=reason
)
# Role management
def edit_role(self, guild_id, role_id, *, reason=None, **fields):
r = Route(
"PATCH", "/guilds/{guild_id}/roles/{role_id}", guild_id=guild_id, role_id=role_id
)
valid_keys = ("name", "permissions", "color", "hoist", "mentionable")
payload = {k: v for k, v in fields.items() if k in valid_keys}
return self.request(r, json=payload, reason=reason)
def delete_role(self, guild_id, role_id, *, reason=None):
r = Route(
"DELETE", "/guilds/{guild_id}/roles/{role_id}", guild_id=guild_id, role_id=role_id
)
return self.request(r, reason=reason)
def replace_roles(self, user_id, guild_id, role_ids, *, reason=None):
return self.edit_member(guild_id=guild_id, user_id=user_id, roles=role_ids, reason=reason)
def create_role(self, guild_id, *, reason=None, **fields):
r = Route("POST", "/guilds/{guild_id}/roles", guild_id=guild_id)
return self.request(r, json=fields, reason=reason)
def move_role_position(self, guild_id, positions, *, reason=None):
r = Route("PATCH", "/guilds/{guild_id}/roles", guild_id=guild_id)
return self.request(r, json=positions, reason=reason)
def add_role(self, guild_id, user_id, role_id, *, reason=None):
r = Route(
"PUT",
"/guilds/{guild_id}/members/{user_id}/roles/{role_id}",
guild_id=guild_id,
user_id=user_id,
role_id=role_id,
)
return self.request(r, reason=reason)
def remove_role(self, guild_id, user_id, role_id, *, reason=None):
r = Route(
"DELETE",
"/guilds/{guild_id}/members/{user_id}/roles/{role_id}",
guild_id=guild_id,
user_id=user_id,
role_id=role_id,
)
return self.request(r, reason=reason)
def edit_channel_permissions(self, channel_id, target, allow, deny, type, *, reason=None):
payload = {"id": target, "allow": allow, "deny": deny, "type": type}
r = Route(
"PUT",
"/channels/{channel_id}/permissions/{target}",
channel_id=channel_id,
target=target,
)
return self.request(r, json=payload, reason=reason)
def delete_channel_permissions(self, channel_id, target, *, reason=None):
r = Route(
"DELETE",
"/channels/{channel_id}/permissions/{target}",
channel_id=channel_id,
target=target,
)
return self.request(r, reason=reason)
# Voice management
def move_member(self, user_id, guild_id, channel_id, *, reason=None):
return self.edit_member(
guild_id=guild_id, user_id=user_id, channel_id=channel_id, reason=reason
)
# Relationship related
def remove_relationship(self, user_id):
r = Route("DELETE", "/users/@me/relationships/{user_id}", user_id=user_id)
return self.request(r)
def add_relationship(self, user_id, type=None):
r = Route("PUT", "/users/@me/relationships/{user_id}", user_id=user_id)
payload = {}
if type is not None:
payload["type"] = type
return self.request(r, json=payload)
def send_friend_request(self, username, discriminator):
r = Route("POST", "/users/@me/relationships")
payload = {"username": username, "discriminator": int(discriminator)}
return self.request(r, json=payload)
# Misc
def application_info(self):
return self.request(Route("GET", "/oauth2/applications/@me"))
async def get_gateway(self, *, encoding="json", v=6, zlib=True):
try:
data = await self.request(Route("GET", "/gateway"))
except HTTPException as exc:
raise GatewayNotFound() from exc
if zlib:
value = "{0}?encoding={1}&v={2}&compress=zlib-stream"
else:
value = "{0}?encoding={1}&v={2}"
return value.format(data["url"], encoding, v)
async def get_bot_gateway(self, *, encoding="json", v=6, zlib=True):
try:
data = await self.request(Route("GET", "/gateway/bot"))
except HTTPException as exc:
raise GatewayNotFound() from exc
if zlib:
value = "{0}?encoding={1}&v={2}&compress=zlib-stream"
else:
value = "{0}?encoding={1}&v={2}"
return data["shards"], value.format(data["url"], encoding, v)
def get_user_info(self, user_id):
return self.request(Route("GET", "/users/{user_id}", user_id=user_id))
def get_user_profile(self, user_id):
return self.request(Route("GET", "/users/{user_id}/profile", user_id=user_id))
def get_mutual_friends(self, user_id):
return self.request(Route("GET", "/users/{user_id}/relationships", user_id=user_id))
def change_hypesquad_house(self, house_id):
payload = {"house_id": house_id}
return self.request(Route("POST", "/hypesquad/online"), json=payload)
def leave_hypesquad_house(self):
return self.request(Route("DELETE", "/hypesquad/online"))

176
discord/invite.py Normal file
View File

@@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from .utils import parse_time
from .mixins import Hashable
from .object import Object
class Invite(Hashable):
"""Represents a Discord :class:`Guild` or :class:`abc.GuildChannel` invite.
Depending on the way this object was created, some of the attributes can
have a value of ``None``.
.. container:: operations
.. describe:: x == y
Checks if two invites are equal.
.. describe:: x != y
Checks if two invites are not equal.
.. describe:: hash(x)
Returns the invite hash.
.. describe:: str(x)
Returns the invite URL.
Attributes
-----------
max_age: :class:`int`
How long the before the invite expires in seconds. A value of 0 indicates that it doesn't expire.
code: :class:`str`
The URL fragment used for the invite.
guild: :class:`Guild`
The guild the invite is for.
revoked: :class:`bool`
Indicates if the invite has been revoked.
created_at: `datetime.datetime`
A datetime object denoting the time the invite was created.
temporary: :class:`bool`
Indicates that the invite grants temporary membership.
If True, members who joined via this invite will be kicked upon disconnect.
uses: :class:`int`
How many times the invite has been used.
max_uses: :class:`int`
How many times the invite can be used.
inviter: :class:`User`
The user who created the invite.
channel: :class:`abc.GuildChannel`
The channel the invite is for.
"""
__slots__ = (
"max_age",
"code",
"guild",
"revoked",
"created_at",
"uses",
"temporary",
"max_uses",
"inviter",
"channel",
"_state",
)
def __init__(self, *, state, data):
self._state = state
self.max_age = data.get("max_age")
self.code = data.get("code")
self.guild = data.get("guild")
self.revoked = data.get("revoked")
self.created_at = parse_time(data.get("created_at"))
self.temporary = data.get("temporary")
self.uses = data.get("uses")
self.max_uses = data.get("max_uses")
inviter_data = data.get("inviter")
self.inviter = None if inviter_data is None else self._state.store_user(inviter_data)
self.channel = data.get("channel")
@classmethod
def from_incomplete(cls, *, state, data):
guild_id = int(data["guild"]["id"])
channel_id = int(data["channel"]["id"])
guild = state._get_guild(guild_id)
if guild is not None:
channel = guild.get_channel(channel_id)
else:
guild = Object(id=guild_id)
channel = Object(id=channel_id)
guild.name = data["guild"]["name"]
guild.splash = data["guild"]["splash"]
guild.splash_url = ""
if guild.splash:
guild.splash_url = "https://cdn.discordapp.com/splashes/{0.id}/{0.splash}.jpg?size=2048".format(
guild
)
channel.name = data["channel"]["name"]
data["guild"] = guild
data["channel"] = channel
return cls(state=state, data=data)
def __str__(self):
return self.url
def __repr__(self):
return "<Invite code={0.code!r}>".format(self)
def __hash__(self):
return hash(self.code)
@property
def id(self):
"""Returns the proper code portion of the invite."""
return self.code
@property
def url(self):
"""A property that retrieves the invite URL."""
return "http://discord.gg/" + self.code
async def delete(self, *, reason=None):
"""|coro|
Revokes the instant invite.
You must have the :attr:`~Permissions.manage_channels` permission to do this.
Parameters
-----------
reason: Optional[str]
The reason for deleting this invite. Shows up on the audit log.
Raises
-------
Forbidden
You do not have permissions to revoke invites.
NotFound
The invite is invalid or expired.
HTTPException
Revoking the invite failed.
"""
await self._state.http.delete_invite(self.code, reason=reason)

489
discord/iterators.py Normal file
View File

@@ -0,0 +1,489 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import asyncio
import datetime
from .errors import NoMoreItems
from .utils import time_snowflake, maybe_coroutine
from .object import Object
from .audit_logs import AuditLogEntry
class _AsyncIterator:
__slots__ = ()
def get(self, **attrs):
def predicate(elem):
for attr, val in attrs.items():
nested = attr.split("__")
obj = elem
for attribute in nested:
obj = getattr(obj, attribute)
if obj != val:
return False
return True
return self.find(predicate)
async def find(self, predicate):
while True:
try:
elem = await self.next()
except NoMoreItems:
return None
ret = await maybe_coroutine(predicate, elem)
if ret:
return elem
def map(self, func):
return _MappedAsyncIterator(self, func)
def filter(self, predicate):
return _FilteredAsyncIterator(self, predicate)
async def flatten(self):
ret = []
while True:
try:
item = await self.next()
except NoMoreItems:
return ret
else:
ret.append(item)
def __aiter__(self):
return self
async def __anext__(self):
try:
msg = await self.next()
except NoMoreItems:
raise StopAsyncIteration()
else:
return msg
def _identity(x):
return x
class _MappedAsyncIterator(_AsyncIterator):
def __init__(self, iterator, func):
self.iterator = iterator
self.func = func
async def next(self):
# this raises NoMoreItems and will propagate appropriately
item = await self.iterator.next()
return await maybe_coroutine(self.func, item)
class _FilteredAsyncIterator(_AsyncIterator):
def __init__(self, iterator, predicate):
self.iterator = iterator
if predicate is None:
predicate = _identity
self.predicate = predicate
async def next(self):
getter = self.iterator.next
pred = self.predicate
while True:
# propagate NoMoreItems similar to _MappedAsyncIterator
item = await getter()
ret = await maybe_coroutine(pred, item)
if ret:
return item
class ReactionIterator(_AsyncIterator):
def __init__(self, message, emoji, limit=100, after=None):
self.message = message
self.limit = limit
self.after = after
state = message._state
self.getter = state.http.get_reaction_users
self.state = state
self.emoji = emoji
self.guild = message.guild
self.channel_id = message.channel.id
self.users = asyncio.Queue(loop=state.loop)
async def next(self):
if self.users.empty():
await self.fill_users()
try:
return self.users.get_nowait()
except asyncio.QueueEmpty:
raise NoMoreItems()
async def fill_users(self):
# this is a hack because >circular imports<
from .user import User
if self.limit > 0:
retrieve = self.limit if self.limit <= 100 else 100
after = self.after.id if self.after else None
data = await self.getter(
self.message.id, self.channel_id, self.emoji, retrieve, after=after
)
if data:
self.limit -= retrieve
self.after = Object(id=int(data[-1]["id"]))
if self.guild is None:
for element in reversed(data):
await self.users.put(User(state=self.state, data=element))
else:
for element in reversed(data):
member_id = int(element["id"])
member = self.guild.get_member(member_id)
if member is not None:
await self.users.put(member)
else:
await self.users.put(User(state=self.state, data=element))
class HistoryIterator(_AsyncIterator):
"""Iterator for receiving a channel's message history.
The messages endpoint has two behaviours we care about here:
If `before` is specified, the messages endpoint returns the `limit`
newest messages before `before`, sorted with newest first. For filling over
100 messages, update the `before` parameter to the oldest message received.
Messages will be returned in order by time.
If `after` is specified, it returns the `limit` oldest messages after
`after`, sorted with newest first. For filling over 100 messages, update the
`after` parameter to the newest message received. If messages are not
reversed, they will be out of order (99-0, 199-100, so on)
A note that if both before and after are specified, before is ignored by the
messages endpoint.
Parameters
-----------
messageable: :class:`abc.Messageable`
Messageable class to retrieve message history fro.
limit : int
Maximum number of messages to retrieve
before : :class:`Message` or id-like
Message before which all messages must be.
after : :class:`Message` or id-like
Message after which all messages must be.
around : :class:`Message` or id-like
Message around which all messages must be. Limit max 101. Note that if
limit is an even number, this will return at most limit+1 messages.
reverse: bool
If set to true, return messages in oldest->newest order. Recommended
when using with "after" queries with limit over 100, otherwise messages
will be out of order.
"""
def __init__(self, messageable, limit, before=None, after=None, around=None, reverse=None):
if isinstance(before, datetime.datetime):
before = Object(id=time_snowflake(before, high=False))
if isinstance(after, datetime.datetime):
after = Object(id=time_snowflake(after, high=True))
if isinstance(around, datetime.datetime):
around = Object(id=time_snowflake(around))
self.messageable = messageable
self.limit = limit
self.before = before
self.after = after
self.around = around
if reverse is None:
self.reverse = after is not None
else:
self.reverse = reverse
self._filter = None # message dict -> bool
self.state = self.messageable._state
self.logs_from = self.state.http.logs_from
self.messages = asyncio.Queue(loop=self.state.loop)
if self.around:
if self.limit is None:
raise ValueError("history does not support around with limit=None")
if self.limit > 101:
raise ValueError("history max limit 101 when specifying around parameter")
elif self.limit == 101:
self.limit = 100 # Thanks discord
elif self.limit == 1:
raise ValueError("Use get_message.")
self._retrieve_messages = self._retrieve_messages_around_strategy
if self.before and self.after:
self._filter = lambda m: self.after.id < int(m["id"]) < self.before.id
elif self.before:
self._filter = lambda m: int(m["id"]) < self.before.id
elif self.after:
self._filter = lambda m: self.after.id < int(m["id"])
elif self.before and self.after:
if self.reverse:
self._retrieve_messages = self._retrieve_messages_after_strategy
self._filter = lambda m: int(m["id"]) < self.before.id
else:
self._retrieve_messages = self._retrieve_messages_before_strategy
self._filter = lambda m: int(m["id"]) > self.after.id
elif self.after:
self._retrieve_messages = self._retrieve_messages_after_strategy
else:
self._retrieve_messages = self._retrieve_messages_before_strategy
async def next(self):
if self.messages.empty():
await self.fill_messages()
try:
return self.messages.get_nowait()
except asyncio.QueueEmpty:
raise NoMoreItems()
def _get_retrieve(self):
l = self.limit
if l is None:
r = 100
elif l <= 100:
r = l
else:
r = 100
self.retrieve = r
return r > 0
async def flatten(self):
# this is similar to fill_messages except it uses a list instead
# of a queue to place the messages in.
result = []
channel = await self.messageable._get_channel()
self.channel = channel
while self._get_retrieve():
data = await self._retrieve_messages(self.retrieve)
if len(data) < 100:
self.limit = 0 # terminate the infinite loop
if self.reverse:
data = reversed(data)
if self._filter:
data = filter(self._filter, data)
for element in data:
result.append(self.state.create_message(channel=channel, data=element))
return result
async def fill_messages(self):
if not hasattr(self, "channel"):
# do the required set up
channel = await self.messageable._get_channel()
self.channel = channel
if self._get_retrieve():
data = await self._retrieve_messages(self.retrieve)
if self.limit is None and len(data) < 100:
self.limit = 0 # terminate the infinite loop
if self.reverse:
data = reversed(data)
if self._filter:
data = filter(self._filter, data)
channel = self.channel
for element in data:
await self.messages.put(self.state.create_message(channel=channel, data=element))
async def _retrieve_messages(self, retrieve):
"""Retrieve messages and update next parameters."""
pass
async def _retrieve_messages_before_strategy(self, retrieve):
"""Retrieve messages using before parameter."""
before = self.before.id if self.before else None
data = await self.logs_from(self.channel.id, retrieve, before=before)
if len(data):
if self.limit is not None:
self.limit -= retrieve
self.before = Object(id=int(data[-1]["id"]))
return data
async def _retrieve_messages_after_strategy(self, retrieve):
"""Retrieve messages using after parameter."""
after = self.after.id if self.after else None
data = await self.logs_from(self.channel.id, retrieve, after=after)
if len(data):
if self.limit is not None:
self.limit -= retrieve
self.after = Object(id=int(data[0]["id"]))
return data
async def _retrieve_messages_around_strategy(self, retrieve):
"""Retrieve messages using around parameter."""
if self.around:
around = self.around.id if self.around else None
data = await self.logs_from(self.channel.id, retrieve, around=around)
self.around = None
return data
return []
class AuditLogIterator(_AsyncIterator):
def __init__(
self,
guild,
limit=None,
before=None,
after=None,
reverse=None,
user_id=None,
action_type=None,
):
if isinstance(before, datetime.datetime):
before = Object(id=time_snowflake(before, high=False))
if isinstance(after, datetime.datetime):
after = Object(id=time_snowflake(after, high=True))
self.guild = guild
self.loop = guild._state.loop
self.request = guild._state.http.get_audit_logs
self.limit = limit
self.before = before
self.user_id = user_id
self.action_type = action_type
self.after = after
self._users = {}
self._state = guild._state
if reverse is None:
self.reverse = after is not None
else:
self.reverse = reverse
self._filter = None # entry dict -> bool
self.entries = asyncio.Queue(loop=self.loop)
if self.before and self.after:
if self.reverse:
self._strategy = self._after_strategy
self._filter = lambda m: int(m["id"]) < self.before.id
else:
self._strategy = self._before_strategy
self._filter = lambda m: int(m["id"]) > self.after.id
elif self.after:
self._strategy = self._after_strategy
else:
self._strategy = self._before_strategy
async def _before_strategy(self, retrieve):
before = self.before.id if self.before else None
data = await self.request(
self.guild.id,
limit=retrieve,
user_id=self.user_id,
action_type=self.action_type,
before=before,
)
entries = data.get("audit_log_entries", [])
if len(data) and entries:
if self.limit is not None:
self.limit -= retrieve
self.before = Object(id=int(entries[-1]["id"]))
return data.get("users", []), entries
async def _after_strategy(self, retrieve):
after = self.after.id if self.after else None
data = await self.request(
self.guild.id,
limit=retrieve,
user_id=self.user_id,
action_type=self.action_type,
after=after,
)
entries = data.get("audit_log_entries", [])
if len(data) and entries:
if self.limit is not None:
self.limit -= retrieve
self.after = Object(id=int(entries[0]["id"]))
return data.get("users", []), entries
async def next(self):
if self.entries.empty():
await self._fill()
try:
return self.entries.get_nowait()
except asyncio.QueueEmpty:
raise NoMoreItems()
def _get_retrieve(self):
l = self.limit
if l is None:
r = 100
elif l <= 100:
r = l
else:
r = 100
self.retrieve = r
return r > 0
async def _fill(self):
from .user import User
if self._get_retrieve():
users, data = await self._strategy(self.retrieve)
if self.limit is None and len(data) < 100:
self.limit = 0 # terminate the infinite loop
if self.reverse:
data = reversed(data)
if self._filter:
data = filter(self._filter, data)
for user in users:
u = User(data=user, state=self._state)
self._users[u.id] = u
for element in data:
# TODO: remove this if statement later
if element["action_type"] is None:
continue
await self.entries.put(
AuditLogEntry(data=element, users=self._users, guild=self.guild)
)

621
discord/member.py Normal file
View File

@@ -0,0 +1,621 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import itertools
import discord.abc
from . import utils
from .user import BaseUser, User
from .activity import create_activity
from .permissions import Permissions
from .enums import Status, try_enum
from .colour import Colour
from .object import Object
class VoiceState:
"""Represents a Discord user's voice state.
Attributes
------------
deaf: :class:`bool`
Indicates if the user is currently deafened by the guild.
mute: :class:`bool`
Indicates if the user is currently muted by the guild.
self_mute: :class:`bool`
Indicates if the user is currently muted by their own accord.
self_deaf: :class:`bool`
Indicates if the user is currently deafened by their own accord.
afk: :class:`bool`
Indicates if the user is currently in the AFK channel in the guild.
channel: :class:`VoiceChannel`
The voice channel that the user is currently connected to. None if the user
is not currently in a voice channel.
"""
__slots__ = ("session_id", "deaf", "mute", "self_mute", "self_deaf", "afk", "channel")
def __init__(self, *, data, channel=None):
self.session_id = data.get("session_id")
self._update(data, channel)
def _update(self, data, channel):
self.self_mute = data.get("self_mute", False)
self.self_deaf = data.get("self_deaf", False)
self.afk = data.get("suppress", False)
self.mute = data.get("mute", False)
self.deaf = data.get("deaf", False)
self.channel = channel
def __repr__(self):
return "<VoiceState self_mute={0.self_mute} self_deaf={0.self_deaf} channel={0.channel!r}>".format(
self
)
def flatten_user(cls):
for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()):
# ignore private/special methods
if attr.startswith("_"):
continue
# don't override what we already have
if attr in cls.__dict__:
continue
# if it's a slotted attribute or a property, redirect it
# slotted members are implemented as member_descriptors in Type.__dict__
if not hasattr(value, "__annotations__"):
def getter(self, x=attr):
return getattr(self._user, x)
setattr(cls, attr, property(getter, doc="Equivalent to :attr:`User.%s`" % attr))
else:
# probably a member function by now
def generate_function(x):
def general(self, *args, **kwargs):
return getattr(self._user, x)(*args, **kwargs)
general.__name__ = x
return general
func = generate_function(attr)
func.__doc__ = value.__doc__
setattr(cls, attr, func)
return cls
_BaseUser = discord.abc.User
@flatten_user
class Member(discord.abc.Messageable, _BaseUser):
"""Represents a Discord member to a :class:`Guild`.
This implements a lot of the functionality of :class:`User`.
.. container:: operations
.. describe:: x == y
Checks if two members are equal.
Note that this works with :class:`User` instances too.
.. describe:: x != y
Checks if two members are not equal.
Note that this works with :class:`User` instances too.
.. describe:: hash(x)
Returns the member's hash.
.. describe:: str(x)
Returns the member's name with the discriminator.
Attributes
----------
joined_at: `datetime.datetime`
A datetime object that specifies the date and time in UTC that the member joined the guild for
the first time.
activities: Tuple[Union[:class:`Game`, :class:`Streaming`, :class:`Spotify`, :class:`Activity`]]
The activities that the user is currently doing.
guild: :class:`Guild`
The guild that the member belongs to.
nick: Optional[:class:`str`]
The guild specific nickname of the user.
"""
__slots__ = (
"_roles",
"joined_at",
"_client_status",
"activities",
"guild",
"nick",
"_user",
"_state",
)
def __init__(self, *, data, guild, state):
self._state = state
self._user = state.store_user(data["user"])
self.guild = guild
self.joined_at = utils.parse_time(data.get("joined_at"))
self._update_roles(data)
self._client_status = {None: Status.offline}
self.activities = tuple(map(create_activity, data.get("activities", [])))
self.nick = data.get("nick", None)
def __str__(self):
return str(self._user)
def __repr__(self):
return (
"<Member id={1.id} name={1.name!r} discriminator={1.discriminator!r}"
" bot={1.bot} nick={0.nick!r} guild={0.guild!r}>".format(self, self._user)
)
def __eq__(self, other):
return isinstance(other, _BaseUser) and other.id == self.id
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self._user)
@classmethod
def _copy(cls, member):
self = cls.__new__(cls) # to bypass __init__
self._roles = utils.SnowflakeList(member._roles, is_sorted=True)
self.joined_at = member.joined_at
self._client_status = member._client_status.copy()
self.guild = member.guild
self.nick = member.nick
self.activities = member.activities
self._state = member._state
self._user = User._copy(member._user)
return self
async def _get_channel(self):
ch = await self.create_dm()
return ch
def _update_roles(self, data):
self._roles = utils.SnowflakeList(map(int, data["roles"]))
def _update(self, data, user=None):
if user:
self._user.name = user["username"]
self._user.discriminator = user["discriminator"]
self._user.avatar = user["avatar"]
self._user.bot = user.get("bot", False)
# the nickname change is optional,
# if it isn't in the payload then it didn't change
try:
self.nick = data["nick"]
except KeyError:
pass
self._update_roles(data)
def _presence_update(self, data, user):
self.activities = tuple(map(create_activity, data.get("activities", [])))
self._client_status = {key: value for key, value in data.get("client_status", {}).items()}
self._client_status[None] = data["status"]
if len(user) > 1:
u = self._user
u.name = user.get("username", u.name)
u.avatar = user.get("avatar", u.avatar)
u.discriminator = user.get("discriminator", u.discriminator)
@property
def status(self):
""":class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead."""
return try_enum(Status, self._client_status[None])
@status.setter
def status(self, value):
# internal use only
self._client_status[None] = str(value)
@property
def mobile_status(self):
""":class:`Status`: The member's status on a mobile device, if applicable."""
return try_enum(Status, self._client_status.get("mobile", "offline"))
@property
def desktop_status(self):
""":class:`Status`: The member's status on the desktop client, if applicable."""
return try_enum(Status, self._client_status.get("desktop", "offline"))
@property
def web_status(self):
""":class:`Status`: The member's status on the web client, if applicable."""
return try_enum(Status, self._client_status.get("web", "offline"))
def is_on_mobile(self):
""":class:`bool`: A helper function that determines if a member is active on a mobile device."""
return "mobile" in self._client_status
@property
def colour(self):
"""A property that returns a :class:`Colour` denoting the rendered colour
for the member. If the default colour is the one rendered then an instance
of :meth:`Colour.default` is returned.
There is an alias for this under ``color``.
"""
roles = self.roles[1:] # remove @everyone
# highest order of the colour is the one that gets rendered.
# if the highest is the default colour then the next one with a colour
# is chosen instead
for role in reversed(roles):
if role.colour.value:
return role.colour
return Colour.default()
color = colour
@property
def roles(self):
"""A :class:`list` of :class:`Role` that the member belongs to. Note
that the first element of this list is always the default '@everyone'
role.
These roles are sorted by their position in the role hierarchy.
"""
result = []
g = self.guild
for role_id in self._roles:
role = g.get_role(role_id)
if role:
result.append(role)
result.append(g.default_role)
result.sort()
return result
@property
def mention(self):
"""Returns a string that mentions the member."""
if self.nick:
return "<@!%s>" % self.id
return "<@%s>" % self.id
@property
def display_name(self):
"""Returns the user's display name.
For regular users this is just their username, but
if they have a guild specific nickname then that
is returned instead.
"""
return self.nick if self.nick is not None else self.name
@property
def activity(self):
"""Returns a class Union[:class:`Game`, :class:`Streaming`, :class:`Spotify`, :class:`Activity`] for the primary
activity the user is currently doing. Could be None if no activity is being done.
.. note::
A user may have multiple activities, these can be accessed under :attr:`activities`.
"""
if self.activities:
return self.activities[0]
def mentioned_in(self, message):
"""Checks if the member is mentioned in the specified message.
Parameters
-----------
message: :class:`Message`
The message to check if you're mentioned in.
"""
if self._user.mentioned_in(message):
return True
for role in message.role_mentions:
has_role = utils.get(self.roles, id=role.id) is not None
if has_role:
return True
return False
def permissions_in(self, channel):
"""An alias for :meth:`abc.GuildChannel.permissions_for`.
Basically equivalent to:
.. code-block:: python3
channel.permissions_for(self)
Parameters
-----------
channel
The channel to check your permissions for.
"""
return channel.permissions_for(self)
@property
def top_role(self):
"""Returns the member's highest role.
This is useful for figuring where a member stands in the role
hierarchy chain.
"""
return self.roles[-1]
@property
def guild_permissions(self):
"""Returns the member's guild permissions.
This only takes into consideration the guild permissions
and not most of the implied permissions or any of the
channel permission overwrites. For 100% accurate permission
calculation, please use either :meth:`permissions_in` or
:meth:`abc.GuildChannel.permissions_for`.
This does take into consideration guild ownership and the
administrator implication.
"""
if self.guild.owner == self:
return Permissions.all()
base = Permissions.none()
for r in self.roles:
base.value |= r.permissions.value
if base.administrator:
return Permissions.all()
return base
@property
def voice(self):
"""Optional[:class:`VoiceState`]: Returns the member's current voice state."""
return self.guild._voice_state_for(self._user.id)
async def ban(self, **kwargs):
"""|coro|
Bans this member. Equivalent to :meth:`Guild.ban`
"""
await self.guild.ban(self, **kwargs)
async def unban(self, *, reason=None):
"""|coro|
Unbans this member. Equivalent to :meth:`Guild.unban`
"""
await self.guild.unban(self, reason=reason)
async def kick(self, *, reason=None):
"""|coro|
Kicks this member. Equivalent to :meth:`Guild.kick`
"""
await self.guild.kick(self, reason=reason)
async def edit(self, *, reason=None, **fields):
"""|coro|
Edits the member's data.
Depending on the parameter passed, this requires different permissions listed below:
+---------------+--------------------------------------+
| Parameter | Permission |
+---------------+--------------------------------------+
| nick | :attr:`Permissions.manage_nicknames` |
+---------------+--------------------------------------+
| mute | :attr:`Permissions.mute_members` |
+---------------+--------------------------------------+
| deafen | :attr:`Permissions.deafen_members` |
+---------------+--------------------------------------+
| roles | :attr:`Permissions.manage_roles` |
+---------------+--------------------------------------+
| voice_channel | :attr:`Permissions.move_members` |
+---------------+--------------------------------------+
All parameters are optional.
Parameters
-----------
nick: str
The member's new nickname. Use ``None`` to remove the nickname.
mute: bool
Indicates if the member should be guild muted or un-muted.
deafen: bool
Indicates if the member should be guild deafened or un-deafened.
roles: List[:class:`Roles`]
The member's new list of roles. This *replaces* the roles.
voice_channel: :class:`VoiceChannel`
The voice channel to move the member to.
reason: Optional[str]
The reason for editing this member. Shows up on the audit log.
Raises
-------
Forbidden
You do not have the proper permissions to the action requested.
HTTPException
The operation failed.
"""
http = self._state.http
guild_id = self.guild.id
payload = {}
try:
nick = fields["nick"]
except KeyError:
# nick not present so...
pass
else:
nick = nick if nick else ""
if self._state.self_id == self.id:
await http.change_my_nickname(guild_id, nick, reason=reason)
else:
payload["nick"] = nick
deafen = fields.get("deafen")
if deafen is not None:
payload["deaf"] = deafen
mute = fields.get("mute")
if mute is not None:
payload["mute"] = mute
try:
vc = fields["voice_channel"]
except KeyError:
pass
else:
payload["channel_id"] = vc.id
try:
roles = fields["roles"]
except KeyError:
pass
else:
payload["roles"] = tuple(r.id for r in roles)
await http.edit_member(guild_id, self.id, reason=reason, **payload)
# TODO: wait for WS event for modify-in-place behaviour
async def move_to(self, channel, *, reason=None):
"""|coro|
Moves a member to a new voice channel (they must be connected first).
You must have the :attr:`~Permissions.move_members` permission to
use this.
This raises the same exceptions as :meth:`edit`.
Parameters
-----------
channel: :class:`VoiceChannel`
The new voice channel to move the member to.
reason: Optional[str]
The reason for doing this action. Shows up on the audit log.
"""
await self.edit(voice_channel=channel, reason=reason)
async def add_roles(self, *roles, reason=None, atomic=True):
r"""|coro|
Gives the member a number of :class:`Role`\s.
You must have the :attr:`~Permissions.manage_roles` permission to
use this.
Parameters
-----------
\*roles
An argument list of :class:`abc.Snowflake` representing a :class:`Role`
to give to the member.
reason: Optional[str]
The reason for adding these roles. Shows up on the audit log.
atomic: bool
Whether to atomically add roles. This will ensure that multiple
operations will always be applied regardless of the current
state of the cache.
Raises
-------
Forbidden
You do not have permissions to add these roles.
HTTPException
Adding roles failed.
"""
if not atomic:
new_roles = utils._unique(Object(id=r.id) for s in (self.roles[1:], roles) for r in s)
await self.edit(roles=new_roles, reason=reason)
else:
req = self._state.http.add_role
guild_id = self.guild.id
user_id = self.id
for role in roles:
await req(guild_id, user_id, role.id, reason=reason)
async def remove_roles(self, *roles, reason=None, atomic=True):
r"""|coro|
Removes :class:`Role`\s from this member.
You must have the :attr:`~Permissions.manage_roles` permission to
use this.
Parameters
-----------
\*roles
An argument list of :class:`abc.Snowflake` representing a :class:`Role`
to remove from the member.
reason: Optional[str]
The reason for removing these roles. Shows up on the audit log.
atomic: bool
Whether to atomically remove roles. This will ensure that multiple
operations will always be applied regardless of the current
state of the cache.
Raises
-------
Forbidden
You do not have permissions to remove these roles.
HTTPException
Removing the roles failed.
"""
if not atomic:
new_roles = [Object(id=r.id) for r in self.roles[1:]] # remove @everyone
for role in roles:
try:
new_roles.remove(Object(id=role.id))
except ValueError:
pass
await self.edit(roles=new_roles, reason=reason)
else:
req = self._state.http.remove_role
guild_id = self.guild.id
user_id = self.id
for role in roles:
await req(guild_id, user_id, role.id, reason=reason)

799
discord/message.py Normal file
View File

@@ -0,0 +1,799 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import asyncio
import re
from . import utils
from .reaction import Reaction
from .emoji import Emoji, PartialEmoji
from .calls import CallMessage
from .enums import MessageType, try_enum
from .errors import InvalidArgument, ClientException, HTTPException
from .embeds import Embed
class Attachment:
"""Represents an attachment from Discord.
Attributes
------------
id: :class:`int`
The attachment ID.
size: :class:`int`
The attachment size in bytes.
height: Optional[:class:`int`]
The attachment's height, in pixels. Only applicable to images.
width: Optional[:class:`int`]
The attachment's width, in pixels. Only applicable to images.
filename: :class:`str`
The attachment's filename.
url: :class:`str`
The attachment URL. If the message this attachment was attached
to is deleted, then this will 404.
proxy_url: :class:`str`
The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the
case of images. When the message is deleted, this URL might be valid for a few
minutes or not valid at all.
"""
__slots__ = ("id", "size", "height", "width", "filename", "url", "proxy_url", "_http")
def __init__(self, *, data, state):
self.id = int(data["id"])
self.size = data["size"]
self.height = data.get("height")
self.width = data.get("width")
self.filename = data["filename"]
self.url = data.get("url")
self.proxy_url = data.get("proxy_url")
self._http = state.http
def is_spoiler(self):
""":class:`bool`: Whether this attachment contains a spoiler."""
return self.filename.startswith("SPOILER_")
async def save(self, fp, *, seek_begin=True):
"""|coro|
Saves this attachment into a file-like object.
Parameters
-----------
fp: Union[BinaryIO, str]
The file-like object to save this attachment to or the filename
to use. If a filename is passed then a file is created with that
filename and used instead.
seek_begin: bool
Whether to seek to the beginning of the file after saving is
successfully done.
Raises
--------
HTTPException
Saving the attachment failed.
NotFound
The attachment was deleted.
Returns
--------
int
The number of bytes written.
"""
data = await self._http.get_attachment(self.url)
if isinstance(fp, str):
with open(fp, "wb") as f:
return f.write(data)
else:
written = fp.write(data)
if seek_begin:
fp.seek(0)
return written
class Message:
r"""Represents a message from Discord.
There should be no need to create one of these manually.
Attributes
-----------
tts: :class:`bool`
Specifies if the message was done with text-to-speech.
type: :class:`MessageType`
The type of message. In most cases this should not be checked, but it is helpful
in cases where it might be a system message for :attr:`system_content`.
author
A :class:`Member` that sent the message. If :attr:`channel` is a
private channel or the user has the left the guild, then it is a :class:`User` instead.
content: :class:`str`
The actual contents of the message.
nonce
The value used by the discord guild and the client to verify that the message is successfully sent.
This is typically non-important.
embeds: List[:class:`Embed`]
A list of embeds the message has.
channel
The :class:`TextChannel` that the message was sent from.
Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message.
call: Optional[:class:`CallMessage`]
The call that the message refers to. This is only applicable to messages of type
:attr:`MessageType.call`.
mention_everyone: :class:`bool`
Specifies if the message mentions everyone.
.. note::
This does not check if the ``@everyone`` or the ``@here`` text is in the message itself.
Rather this boolean indicates if either the ``@everyone`` or the ``@here`` text is in the message
**and** it did end up mentioning.
mentions: :class:`list`
A list of :class:`Member` that were mentioned. If the message is in a private message
then the list will be of :class:`User` instead. For messages that are not of type
:attr:`MessageType.default`\, this array can be used to aid in system messages.
For more information, see :attr:`system_content`.
.. warning::
The order of the mentions list is not in any particular order so you should
not rely on it. This is a discord limitation, not one with the library.
channel_mentions: :class:`list`
A list of :class:`abc.GuildChannel` that were mentioned. If the message is in a private message
then the list is always empty.
role_mentions: :class:`list`
A list of :class:`Role` that were mentioned. If the message is in a private message
then the list is always empty.
id: :class:`int`
The message ID.
webhook_id: Optional[:class:`int`]
If this message was sent by a webhook, then this is the webhook ID's that sent this
message.
attachments: List[:class:`Attachment`]
A list of attachments given to a message.
pinned: :class:`bool`
Specifies if the message is currently pinned.
reactions : List[:class:`Reaction`]
Reactions to a message. Reactions can be either custom emoji or standard unicode emoji.
activity: Optional[:class:`dict`]
The activity associated with this message. Sent with Rich-Presence related messages that for
example, request joining, spectating, or listening to or with another member.
It is a dictionary with the following optional keys:
- ``type``: An integer denoting the type of message activity being requested.
- ``party_id``: The party ID associated with the party.
application: Optional[:class:`dict`]
The rich presence enabled application associated with this message.
It is a dictionary with the following keys:
- ``id``: A string representing the application's ID.
- ``name``: A string representing the application's name.
- ``description``: A string representing the application's description.
- ``icon``: A string representing the icon ID of the application.
- ``cover_image``: A string representing the embed's image asset ID.
"""
__slots__ = (
"_edited_timestamp",
"tts",
"content",
"channel",
"webhook_id",
"mention_everyone",
"embeds",
"id",
"mentions",
"author",
"_cs_channel_mentions",
"_cs_raw_mentions",
"attachments",
"_cs_clean_content",
"_cs_raw_channel_mentions",
"nonce",
"pinned",
"role_mentions",
"_cs_raw_role_mentions",
"type",
"call",
"_cs_system_content",
"_cs_guild",
"_state",
"reactions",
"application",
"activity",
)
def __init__(self, *, state, channel, data):
self._state = state
self.id = int(data["id"])
self.webhook_id = utils._get_as_snowflake(data, "webhook_id")
self.reactions = [Reaction(message=self, data=d) for d in data.get("reactions", [])]
self.application = data.get("application")
self.activity = data.get("activity")
self._update(channel, data)
def __repr__(self):
return "<Message id={0.id} pinned={0.pinned} author={0.author!r}>".format(self)
def _try_patch(self, data, key, transform=None):
try:
value = data[key]
except KeyError:
pass
else:
if transform is None:
setattr(self, key, value)
else:
setattr(self, key, transform(value))
def _add_reaction(self, data, emoji, user_id):
reaction = utils.find(lambda r: r.emoji == emoji, self.reactions)
is_me = data["me"] = user_id == self._state.self_id
if reaction is None:
reaction = Reaction(message=self, data=data, emoji=emoji)
self.reactions.append(reaction)
else:
reaction.count += 1
if is_me:
reaction.me = is_me
return reaction
def _remove_reaction(self, data, emoji, user_id):
reaction = utils.find(lambda r: r.emoji == emoji, self.reactions)
if reaction is None:
# already removed?
raise ValueError("Emoji already removed?")
# if reaction isn't in the list, we crash. This means discord
# sent bad data, or we stored improperly
reaction.count -= 1
if user_id == self._state.self_id:
reaction.me = False
if reaction.count == 0:
# this raises ValueError if something went wrong as well.
self.reactions.remove(reaction)
return reaction
def _update(self, channel, data):
self.channel = channel
self._edited_timestamp = utils.parse_time(data.get("edited_timestamp"))
self._try_patch(data, "pinned")
self._try_patch(data, "application")
self._try_patch(data, "activity")
self._try_patch(data, "mention_everyone")
self._try_patch(data, "tts")
self._try_patch(data, "type", lambda x: try_enum(MessageType, x))
self._try_patch(data, "content")
self._try_patch(
data, "attachments", lambda x: [Attachment(data=a, state=self._state) for a in x]
)
self._try_patch(data, "embeds", lambda x: list(map(Embed.from_data, x)))
self._try_patch(data, "nonce")
for handler in ("author", "mentions", "mention_roles", "call"):
try:
getattr(self, "_handle_%s" % handler)(data[handler])
except KeyError:
continue
# clear the cached properties
cached = filter(lambda attr: attr.startswith("_cs_"), self.__slots__)
for attr in cached:
try:
delattr(self, attr)
except AttributeError:
pass
def _handle_author(self, author):
self.author = self._state.store_user(author)
if self.guild is not None:
found = self.guild.get_member(self.author.id)
if found is not None:
self.author = found
def _handle_mentions(self, mentions):
self.mentions = []
if self.guild is None:
self.mentions = [self._state.store_user(m) for m in mentions]
return
for mention in filter(None, mentions):
id_search = int(mention["id"])
member = self.guild.get_member(id_search)
if member is not None:
self.mentions.append(member)
def _handle_mention_roles(self, role_mentions):
self.role_mentions = []
if self.guild is not None:
for role_id in map(int, role_mentions):
role = self.guild.get_role(role_id)
if role is not None:
self.role_mentions.append(role)
def _handle_call(self, call):
if call is None or self.type is not MessageType.call:
self.call = None
return
# we get the participant source from the mentions array or
# the author
participants = []
for uid in map(int, call.get("participants", [])):
if uid == self.author.id:
participants.append(self.author)
else:
user = utils.find(lambda u: u.id == uid, self.mentions)
if user is not None:
participants.append(user)
call["participants"] = participants
self.call = CallMessage(message=self, **call)
@utils.cached_slot_property("_cs_guild")
def guild(self):
"""Optional[:class:`Guild`]: The guild that the message belongs to, if applicable."""
return getattr(self.channel, "guild", None)
@utils.cached_slot_property("_cs_raw_mentions")
def raw_mentions(self):
"""A property that returns an array of user IDs matched with
the syntax of <@user_id> in the message content.
This allows you to receive the user IDs of mentioned users
even in a private message context.
"""
return [int(x) for x in re.findall(r"<@!?([0-9]+)>", self.content)]
@utils.cached_slot_property("_cs_raw_channel_mentions")
def raw_channel_mentions(self):
"""A property that returns an array of channel IDs matched with
the syntax of <#channel_id> in the message content.
"""
return [int(x) for x in re.findall(r"<#([0-9]+)>", self.content)]
@utils.cached_slot_property("_cs_raw_role_mentions")
def raw_role_mentions(self):
"""A property that returns an array of role IDs matched with
the syntax of <@&role_id> in the message content.
"""
return [int(x) for x in re.findall(r"<@&([0-9]+)>", self.content)]
@utils.cached_slot_property("_cs_channel_mentions")
def channel_mentions(self):
if self.guild is None:
return []
it = filter(None, map(self.guild.get_channel, self.raw_channel_mentions))
return utils._unique(it)
@utils.cached_slot_property("_cs_clean_content")
def clean_content(self):
"""A property that returns the content in a "cleaned up"
manner. This basically means that mentions are transformed
into the way the client shows it. e.g. ``<#id>`` will transform
into ``#name``.
This will also transform @everyone and @here mentions into
non-mentions.
"""
transformations = {
re.escape("<#%s>" % channel.id): "#" + channel.name
for channel in self.channel_mentions
}
mention_transforms = {
re.escape("<@%s>" % member.id): "@" + member.display_name for member in self.mentions
}
# add the <@!user_id> cases as well..
second_mention_transforms = {
re.escape("<@!%s>" % member.id): "@" + member.display_name for member in self.mentions
}
transformations.update(mention_transforms)
transformations.update(second_mention_transforms)
if self.guild is not None:
role_transforms = {
re.escape("<@&%s>" % role.id): "@" + role.name for role in self.role_mentions
}
transformations.update(role_transforms)
def repl(obj):
return transformations.get(re.escape(obj.group(0)), "")
pattern = re.compile("|".join(transformations.keys()))
result = pattern.sub(repl, self.content)
transformations = {"@everyone": "@\u200beveryone", "@here": "@\u200bhere"}
def repl2(obj):
return transformations.get(obj.group(0), "")
pattern = re.compile("|".join(transformations.keys()))
return pattern.sub(repl2, result)
@property
def created_at(self):
"""datetime.datetime: The message's creation time in UTC."""
return utils.snowflake_time(self.id)
@property
def edited_at(self):
"""Optional[datetime.datetime]: A naive UTC datetime object containing the edited time of the message."""
return self._edited_timestamp
@property
def jump_url(self):
""":class:`str`: Returns a URL that allows the client to jump to this message."""
guild_id = getattr(self.guild, "id", "@me")
return "https://discordapp.com/channels/{0}/{1.channel.id}/{1.id}".format(guild_id, self)
@utils.cached_slot_property("_cs_system_content")
def system_content(self):
r"""A property that returns the content that is rendered
regardless of the :attr:`Message.type`.
In the case of :attr:`MessageType.default`\, this just returns the
regular :attr:`Message.content`. Otherwise this returns an English
message denoting the contents of the system message.
"""
if self.type is MessageType.default:
return self.content
if self.type is MessageType.pins_add:
return "{0.name} pinned a message to this channel.".format(self.author)
if self.type is MessageType.recipient_add:
return "{0.name} added {1.name} to the group.".format(self.author, self.mentions[0])
if self.type is MessageType.recipient_remove:
return "{0.name} removed {1.name} from the group.".format(
self.author, self.mentions[0]
)
if self.type is MessageType.channel_name_change:
return "{0.author.name} changed the channel name: {0.content}".format(self)
if self.type is MessageType.channel_icon_change:
return "{0.author.name} changed the channel icon.".format(self)
if self.type is MessageType.new_member:
formats = [
"{0} just joined the server - glhf!",
"{0} just joined. Everyone, look busy!",
"{0} just joined. Can I get a heal?",
"{0} joined your party.",
"{0} joined. You must construct additional pylons.",
"Ermagherd. {0} is here.",
"Welcome, {0}. Stay awhile and listen.",
"Welcome, {0}. We were expecting you ( ͡° ͜ʖ ͡°)",
"Welcome, {0}. We hope you brought pizza.",
"Welcome {0}. Leave your weapons by the door.",
"A wild {0} appeared.",
"Swoooosh. {0} just landed.",
"Brace yourselves. {0} just joined the server.",
"{0} just joined. Hide your bananas.",
"{0} just arrived. Seems OP - please nerf.",
"{0} just slid into the server.",
"A {0} has spawned in the server.",
"Big {0} showed up!",
"Wheres {0}? In the server!",
"{0} hopped into the server. Kangaroo!!",
"{0} just showed up. Hold my beer.",
"Challenger approaching - {0} has appeared!",
"It's a bird! It's a plane! Nevermind, it's just {0}.",
"It's {0}! Praise the sun! [T]/",
"Never gonna give {0} up. Never gonna let {0} down.",
"Ha! {0} has joined! You activated my trap card!",
"Cheers, love! {0}'s here!",
"Hey! Listen! {0} has joined!",
"We've been expecting you {0}",
"It's dangerous to go alone, take {0}!",
"{0} has joined the server! It's super effective!",
"Cheers, love! {0} is here!",
"{0} is here, as the prophecy foretold.",
"{0} has arrived. Party's over.",
"Ready player {0}",
"{0} is here to kick butt and chew bubblegum. And {0} is all out of gum.",
"Hello. Is it {0} you're looking for?",
"{0} has joined. Stay a while and listen!",
"Roses are red, violets are blue, {0} joined this server with you",
]
index = int(self.created_at.timestamp()) % len(formats)
return formats[index].format(self.author.name)
if self.type is MessageType.call:
# we're at the call message type now, which is a bit more complicated.
# we can make the assumption that Message.channel is a PrivateChannel
# with the type ChannelType.group or ChannelType.private
call_ended = self.call.ended_timestamp is not None
if self.channel.me in self.call.participants:
return "{0.author.name} started a call.".format(self)
elif call_ended:
return "You missed a call from {0.author.name}".format(self)
else:
return "{0.author.name} started a call \N{EM DASH} Join the call.".format(self)
async def delete(self):
"""|coro|
Deletes the message.
Your own messages could be deleted without any proper permissions. However to
delete other people's messages, you need the :attr:`~Permissions.manage_messages`
permission.
Raises
------
Forbidden
You do not have proper permissions to delete the message.
HTTPException
Deleting the message failed.
"""
await self._state.http.delete_message(self.channel.id, self.id)
async def edit(self, **fields):
"""|coro|
Edits the message.
The content must be able to be transformed into a string via ``str(content)``.
Parameters
-----------
content: Optional[str]
The new content to replace the message with.
Could be ``None`` to remove the content.
embed: Optional[:class:`Embed`]
The new embed to replace the original with.
Could be ``None`` to remove the embed.
delete_after: Optional[float]
If provided, the number of seconds to wait in the background
before deleting the message we just edited. If the deletion fails,
then it is silently ignored.
Raises
-------
HTTPException
Editing the message failed.
"""
try:
content = fields["content"]
except KeyError:
pass
else:
if content is not None:
fields["content"] = str(content)
try:
embed = fields["embed"]
except KeyError:
pass
else:
if embed is not None:
fields["embed"] = embed.to_dict()
data = await self._state.http.edit_message(self.id, self.channel.id, **fields)
self._update(channel=self.channel, data=data)
try:
delete_after = fields["delete_after"]
except KeyError:
pass
else:
if delete_after is not None:
async def delete():
await asyncio.sleep(delete_after, loop=self._state.loop)
try:
await self._state.http.delete_message(self.channel.id, self.id)
except HTTPException:
pass
asyncio.ensure_future(delete(), loop=self._state.loop)
async def pin(self):
"""|coro|
Pins the message.
You must have the :attr:`~Permissions.manage_messages` permission to do
this in a non-private channel context.
Raises
-------
Forbidden
You do not have permissions to pin the message.
NotFound
The message or channel was not found or deleted.
HTTPException
Pinning the message failed, probably due to the channel
having more than 50 pinned messages.
"""
await self._state.http.pin_message(self.channel.id, self.id)
self.pinned = True
async def unpin(self):
"""|coro|
Unpins the message.
You must have the :attr:`~Permissions.manage_messages` permission to do
this in a non-private channel context.
Raises
-------
Forbidden
You do not have permissions to unpin the message.
NotFound
The message or channel was not found or deleted.
HTTPException
Unpinning the message failed.
"""
await self._state.http.unpin_message(self.channel.id, self.id)
self.pinned = False
async def add_reaction(self, emoji):
"""|coro|
Add a reaction to the message.
The emoji may be a unicode emoji or a custom guild :class:`Emoji`.
You must have the :attr:`~Permissions.read_message_history` permission
to use this. If nobody else has reacted to the message using this
emoji, the :attr:`~Permissions.add_reactions` permission is required.
Parameters
------------
emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialEmoji`, str]
The emoji to react with.
Raises
--------
HTTPException
Adding the reaction failed.
Forbidden
You do not have the proper permissions to react to the message.
NotFound
The emoji you specified was not found.
InvalidArgument
The emoji parameter is invalid.
"""
emoji = self._emoji_reaction(emoji)
await self._state.http.add_reaction(self.id, self.channel.id, emoji)
async def remove_reaction(self, emoji, member):
"""|coro|
Remove a reaction by the member from the message.
The emoji may be a unicode emoji or a custom guild :class:`Emoji`.
If the reaction is not your own (i.e. ``member`` parameter is not you) then
the :attr:`~Permissions.manage_messages` permission is needed.
The ``member`` parameter must represent a member and meet
the :class:`abc.Snowflake` abc.
Parameters
------------
emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialEmoji`, str]
The emoji to remove.
member: :class:`abc.Snowflake`
The member for which to remove the reaction.
Raises
--------
HTTPException
Removing the reaction failed.
Forbidden
You do not have the proper permissions to remove the reaction.
NotFound
The member or emoji you specified was not found.
InvalidArgument
The emoji parameter is invalid.
"""
emoji = self._emoji_reaction(emoji)
if member.id == self._state.self_id:
await self._state.http.remove_own_reaction(self.id, self.channel.id, emoji)
else:
await self._state.http.remove_reaction(self.id, self.channel.id, emoji, member.id)
@staticmethod
def _emoji_reaction(emoji):
if isinstance(emoji, Reaction):
emoji = emoji.emoji
if isinstance(emoji, Emoji):
return "%s:%s" % (emoji.name, emoji.id)
if isinstance(emoji, PartialEmoji):
return emoji._as_reaction()
if isinstance(emoji, str):
return emoji # this is okay
raise InvalidArgument(
"emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.".format(
emoji
)
)
async def clear_reactions(self):
"""|coro|
Removes all the reactions from the message.
You need the :attr:`~Permissions.manage_messages` permission to use this.
Raises
--------
HTTPException
Removing the reactions failed.
Forbidden
You do not have the proper permissions to remove all the reactions.
"""
await self._state.http.clear_reactions(self.id, self.channel.id)
def ack(self):
"""|coro|
Marks this message as read.
The user must not be a bot user.
Raises
-------
HTTPException
Acking failed.
ClientException
You must not be a bot user.
"""
state = self._state
if state.is_bot:
raise ClientException("Must not be a bot account to ack messages.")
return state.http.ack_message(self.channel.id, self.id)

44
discord/mixins.py Normal file
View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
class EqualityComparable:
__slots__ = ()
def __eq__(self, other):
return isinstance(other, self.__class__) and other.id == self.id
def __ne__(self, other):
if isinstance(other, self.__class__):
return other.id != self.id
return True
class Hashable(EqualityComparable):
__slots__ = ()
def __hash__(self):
return self.id >> 22

71
discord/object.py Normal file
View File

@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from . import utils
from .mixins import Hashable
class Object(Hashable):
"""Represents a generic Discord object.
The purpose of this class is to allow you to create 'miniature'
versions of data classes if you want to pass in just an ID. Most functions
that take in a specific data class with an ID can also take in this class
as a substitute instead. Note that even though this is the case, not all
objects (if any) actually inherit from this class.
There are also some cases where some websocket events are received
in :issue:`strange order <21>` and when such events happened you would
receive this class rather than the actual data class. These cases are
extremely rare.
.. container:: operations
.. describe:: x == y
Checks if two objects are equal.
.. describe:: x != y
Checks if two objects are not equal.
.. describe:: hash(x)
Returns the object's hash.
Attributes
-----------
id : :class:`str`
The ID of the object.
"""
def __init__(self, id):
self.id = id
@property
def created_at(self):
"""Returns the snowflake's creation time in UTC."""
return utils.snowflake_time(self.id)

286
discord/opus.py Normal file
View File

@@ -0,0 +1,286 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import array
import ctypes
import ctypes.util
import logging
import os.path
import sys
from .errors import DiscordException
log = logging.getLogger(__name__)
c_int_ptr = ctypes.POINTER(ctypes.c_int)
c_int16_ptr = ctypes.POINTER(ctypes.c_int16)
c_float_ptr = ctypes.POINTER(ctypes.c_float)
class EncoderStruct(ctypes.Structure):
pass
EncoderStructPtr = ctypes.POINTER(EncoderStruct)
def _err_lt(result, func, args):
if result < 0:
log.info("error has happened in %s", func.__name__)
raise OpusError(result)
return result
def _err_ne(result, func, args):
ret = args[-1]._obj
if ret.value != 0:
log.info("error has happened in %s", func.__name__)
raise OpusError(ret.value)
return result
# A list of exported functions.
# The first argument is obviously the name.
# The second one are the types of arguments it takes.
# The third is the result type.
# The fourth is the error handler.
exported_functions = [
("opus_strerror", [ctypes.c_int], ctypes.c_char_p, None),
("opus_encoder_get_size", [ctypes.c_int], ctypes.c_int, None),
(
"opus_encoder_create",
[ctypes.c_int, ctypes.c_int, ctypes.c_int, c_int_ptr],
EncoderStructPtr,
_err_ne,
),
(
"opus_encode",
[EncoderStructPtr, c_int16_ptr, ctypes.c_int, ctypes.c_char_p, ctypes.c_int32],
ctypes.c_int32,
_err_lt,
),
("opus_encoder_ctl", None, ctypes.c_int32, _err_lt),
("opus_encoder_destroy", [EncoderStructPtr], None, None),
]
def libopus_loader(name):
# create the library...
lib = ctypes.cdll.LoadLibrary(name)
# register the functions...
for item in exported_functions:
func = getattr(lib, item[0])
try:
if item[1]:
func.argtypes = item[1]
func.restype = item[2]
except KeyError:
pass
try:
if item[3]:
func.errcheck = item[3]
except KeyError:
log.exception("Error assigning check function to %s", func)
return lib
try:
if sys.platform == "win32":
_basedir = os.path.dirname(os.path.abspath(__file__))
_bitness = "x64" if sys.maxsize > 2 ** 32 else "x86"
_filename = os.path.join(_basedir, "bin", "libopus-0.{}.dll".format(_bitness))
_lib = libopus_loader(_filename)
else:
_lib = libopus_loader(ctypes.util.find_library("opus"))
except Exception:
_lib = None
def load_opus(name):
"""Loads the libopus shared library for use with voice.
If this function is not called then the library uses the function
`ctypes.util.find_library`__ and then loads that one
if available.
.. _find library: https://docs.python.org/3.5/library/ctypes.html#finding-shared-libraries
__ `find library`_
Not loading a library leads to voice not working.
This function propagates the exceptions thrown.
Warning
--------
The bitness of the library must match the bitness of your python
interpreter. If the library is 64-bit then your python interpreter
must be 64-bit as well. Usually if there's a mismatch in bitness then
the load will throw an exception.
Note
----
On Windows, the .dll extension is not necessary. However, on Linux
the full extension is required to load the library, e.g. ``libopus.so.1``.
On Linux however, `find library`_ will usually find the library automatically
without you having to call this.
Parameters
----------
name: str
The filename of the shared library.
"""
global _lib
_lib = libopus_loader(name)
def is_loaded():
"""Function to check if opus lib is successfully loaded either
via the ``ctypes.util.find_library`` call of :func:`load_opus`.
This must return ``True`` for voice to work.
Returns
-------
bool
Indicates if the opus library has been loaded.
"""
global _lib
return _lib is not None
class OpusError(DiscordException):
"""An exception that is thrown for libopus related errors.
Attributes
----------
code : :class:`int`
The error code returned.
"""
def __init__(self, code):
self.code = code
msg = _lib.opus_strerror(self.code).decode("utf-8")
log.info('"%s" has happened', msg)
super().__init__(msg)
class OpusNotLoaded(DiscordException):
"""An exception that is thrown for when libopus is not loaded."""
pass
# Some constants...
OK = 0
APPLICATION_AUDIO = 2049
APPLICATION_VOIP = 2048
APPLICATION_LOWDELAY = 2051
CTL_SET_BITRATE = 4002
CTL_SET_BANDWIDTH = 4008
CTL_SET_FEC = 4012
CTL_SET_PLP = 4014
CTL_SET_SIGNAL = 4024
band_ctl = {"narrow": 1101, "medium": 1102, "wide": 1103, "superwide": 1104, "full": 1105}
signal_ctl = {"auto": -1000, "voice": 3001, "music": 3002}
class Encoder:
SAMPLING_RATE = 48000
CHANNELS = 2
FRAME_LENGTH = 20
SAMPLE_SIZE = 4 # (bit_rate / 8) * CHANNELS (bit_rate == 16)
SAMPLES_PER_FRAME = int(SAMPLING_RATE / 1000 * FRAME_LENGTH)
FRAME_SIZE = SAMPLES_PER_FRAME * SAMPLE_SIZE
def __init__(self, application=APPLICATION_AUDIO):
self.application = application
if not is_loaded():
raise OpusNotLoaded()
self._state = self._create_state()
self.set_bitrate(128)
self.set_fec(True)
self.set_expected_packet_loss_percent(0.15)
self.set_bandwidth("full")
self.set_signal_type("auto")
def __del__(self):
if hasattr(self, "_state"):
_lib.opus_encoder_destroy(self._state)
self._state = None
def _create_state(self):
ret = ctypes.c_int()
return _lib.opus_encoder_create(
self.SAMPLING_RATE, self.CHANNELS, self.application, ctypes.byref(ret)
)
def set_bitrate(self, kbps):
kbps = min(128, max(16, int(kbps)))
_lib.opus_encoder_ctl(self._state, CTL_SET_BITRATE, kbps * 1024)
return kbps
def set_bandwidth(self, req):
if req not in band_ctl:
raise KeyError(
"%r is not a valid bandwidth setting. Try one of: %s" % (req, ",".join(band_ctl))
)
k = band_ctl[req]
_lib.opus_encoder_ctl(self._state, CTL_SET_BANDWIDTH, k)
def set_signal_type(self, req):
if req not in signal_ctl:
raise KeyError(
"%r is not a valid signal setting. Try one of: %s" % (req, ",".join(signal_ctl))
)
k = signal_ctl[req]
_lib.opus_encoder_ctl(self._state, CTL_SET_SIGNAL, k)
def set_fec(self, enabled=True):
_lib.opus_encoder_ctl(self._state, CTL_SET_FEC, 1 if enabled else 0)
def set_expected_packet_loss_percent(self, percentage):
_lib.opus_encoder_ctl(self._state, CTL_SET_PLP, min(100, max(0, int(percentage * 100))))
def encode(self, pcm, frame_size):
max_data_bytes = len(pcm)
pcm = ctypes.cast(pcm, c_int16_ptr)
data = (ctypes.c_char * max_data_bytes)()
ret = _lib.opus_encode(self._state, pcm, frame_size, data, max_data_bytes)
return array.array("b", data[:ret]).tobytes()

643
discord/permissions.py Normal file
View File

@@ -0,0 +1,643 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
class Permissions:
"""Wraps up the Discord permission value.
The properties provided are two way. You can set and retrieve individual
bits using the properties as if they were regular bools. This allows
you to edit permissions.
.. container:: operations
.. describe:: x == y
Checks if two permissions are equal.
.. describe:: x != y
Checks if two permissions are not equal.
.. describe:: x <= y
Checks if a permission is a subset of another permission.
.. describe:: x >= y
Checks if a permission is a superset of another permission.
.. describe:: x < y
Checks if a permission is a strict subset of another permission.
.. describe:: x > y
Checks if a permission is a strict superset of another permission.
.. describe:: hash(x)
Return the permission's hash.
.. describe:: iter(x)
Returns an iterator of ``(perm, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Attributes
-----------
value
The raw value. This value is a bit array field of a 53-bit integer
representing the currently available permissions. You should query
permissions via the properties rather than using this raw value.
"""
__slots__ = ("value",)
def __init__(self, permissions=0):
if not isinstance(permissions, int):
raise TypeError(
"Expected int parameter, received %s instead." % permissions.__class__.__name__
)
self.value = permissions
def __eq__(self, other):
return isinstance(other, Permissions) and self.value == other.value
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self.value)
def __repr__(self):
return "<Permissions value=%s>" % self.value
def _perm_iterator(self):
for attr in dir(self):
# check if it's a property, because if so it's a permission
is_property = isinstance(getattr(self.__class__, attr), property)
if is_property:
yield (attr, getattr(self, attr))
def __iter__(self):
return self._perm_iterator()
def is_subset(self, other):
"""Returns True if self has the same or fewer permissions as other."""
if isinstance(other, Permissions):
return (self.value & other.value) == self.value
else:
raise TypeError(
"cannot compare {} with {}".format(
self.__class__.__name__, other.__class__.__name__
)
)
def is_superset(self, other):
"""Returns True if self has the same or more permissions as other."""
if isinstance(other, Permissions):
return (self.value | other.value) == self.value
else:
raise TypeError(
"cannot compare {} with {}".format(
self.__class__.__name__, other.__class__.__name__
)
)
def is_strict_subset(self, other):
"""Returns True if the permissions on other are a strict subset of those on self."""
return self.is_subset(other) and self != other
def is_strict_superset(self, other):
"""Returns True if the permissions on other are a strict superset of those on self."""
return self.is_superset(other) and self != other
__le__ = is_subset
__ge__ = is_superset
__lt__ = is_strict_subset
__gt__ = is_strict_superset
@classmethod
def none(cls):
"""A factory method that creates a :class:`Permissions` with all
permissions set to False."""
return cls(0)
@classmethod
def all(cls):
"""A factory method that creates a :class:`Permissions` with all
permissions set to True."""
return cls(0b01111111111101111111110111111111)
@classmethod
def all_channel(cls):
"""A :class:`Permissions` with all channel-specific permissions set to
True and the guild-specific ones set to False. The guild-specific
permissions are currently:
- manage_guild
- kick_members
- ban_members
- administrator
- change_nickname
- manage_nicknames
"""
return cls(0b00110011111101111111110001010001)
@classmethod
def general(cls):
"""A factory method that creates a :class:`Permissions` with all
"General" permissions from the official Discord UI set to True."""
return cls(0b01111100000000000000000010111111)
@classmethod
def text(cls):
"""A factory method that creates a :class:`Permissions` with all
"Text" permissions from the official Discord UI set to True."""
return cls(0b00000000000001111111110001000000)
@classmethod
def voice(cls):
"""A factory method that creates a :class:`Permissions` with all
"Voice" permissions from the official Discord UI set to True."""
return cls(0b00000011111100000000000100000000)
def update(self, **kwargs):
r"""Bulk updates this permission object.
Allows you to set multiple attributes by using keyword
arguments. The names must be equivalent to the properties
listed. Extraneous key/value pairs will be silently ignored.
Parameters
------------
\*\*kwargs
A list of key/value pairs to bulk update permissions with.
"""
for key, value in kwargs.items():
try:
is_property = isinstance(getattr(self.__class__, key), property)
except AttributeError:
continue
if is_property:
setattr(self, key, value)
def _bit(self, index):
return bool((self.value >> index) & 1)
def _set(self, index, value):
if value is True:
self.value |= 1 << index
elif value is False:
self.value &= ~(1 << index)
else:
raise TypeError("Value to set for Permissions must be a bool.")
def handle_overwrite(self, allow, deny):
# Basically this is what's happening here.
# We have an original bit array, e.g. 1010
# Then we have another bit array that is 'denied', e.g. 1111
# And then we have the last one which is 'allowed', e.g. 0101
# We want original OP denied to end up resulting in
# whatever is in denied to be set to 0.
# So 1010 OP 1111 -> 0000
# Then we take this value and look at the allowed values.
# And whatever is allowed is set to 1.
# So 0000 OP2 0101 -> 0101
# The OP is base & ~denied.
# The OP2 is base | allowed.
self.value = (self.value & ~deny) | allow
@property
def create_instant_invite(self):
"""Returns True if the user can create instant invites."""
return self._bit(0)
@create_instant_invite.setter
def create_instant_invite(self, value):
self._set(0, value)
@property
def kick_members(self):
"""Returns True if the user can kick users from the guild."""
return self._bit(1)
@kick_members.setter
def kick_members(self, value):
self._set(1, value)
@property
def ban_members(self):
"""Returns True if a user can ban users from the guild."""
return self._bit(2)
@ban_members.setter
def ban_members(self, value):
self._set(2, value)
@property
def administrator(self):
"""Returns True if a user is an administrator. This role overrides all other permissions.
This also bypasses all channel-specific overrides.
"""
return self._bit(3)
@administrator.setter
def administrator(self, value):
self._set(3, value)
@property
def manage_channels(self):
"""Returns True if a user can edit, delete, or create channels in the guild.
This also corresponds to the "Manage Channel" channel-specific override."""
return self._bit(4)
@manage_channels.setter
def manage_channels(self, value):
self._set(4, value)
@property
def manage_guild(self):
"""Returns True if a user can edit guild properties."""
return self._bit(5)
@manage_guild.setter
def manage_guild(self, value):
self._set(5, value)
@property
def add_reactions(self):
"""Returns True if a user can add reactions to messages."""
return self._bit(6)
@add_reactions.setter
def add_reactions(self, value):
self._set(6, value)
@property
def view_audit_log(self):
"""Returns True if a user can view the guild's audit log."""
return self._bit(7)
@view_audit_log.setter
def view_audit_log(self, value):
self._set(7, value)
@property
def priority_speaker(self):
"""Returns True if a user can be more easily heard while talking."""
return self._bit(8)
@priority_speaker.setter
def priority_speaker(self, value):
self._set(8, value)
# 1 unused
@property
def read_messages(self):
"""Returns True if a user can read messages from all or specific text channels."""
return self._bit(10)
@read_messages.setter
def read_messages(self, value):
self._set(10, value)
@property
def send_messages(self):
"""Returns True if a user can send messages from all or specific text channels."""
return self._bit(11)
@send_messages.setter
def send_messages(self, value):
self._set(11, value)
@property
def send_tts_messages(self):
"""Returns True if a user can send TTS messages from all or specific text channels."""
return self._bit(12)
@send_tts_messages.setter
def send_tts_messages(self, value):
self._set(12, value)
@property
def manage_messages(self):
"""Returns True if a user can delete or pin messages in a text channel. Note that there are currently no ways to edit other people's messages."""
return self._bit(13)
@manage_messages.setter
def manage_messages(self, value):
self._set(13, value)
@property
def embed_links(self):
"""Returns True if a user's messages will automatically be embedded by Discord."""
return self._bit(14)
@embed_links.setter
def embed_links(self, value):
self._set(14, value)
@property
def attach_files(self):
"""Returns True if a user can send files in their messages."""
return self._bit(15)
@attach_files.setter
def attach_files(self, value):
self._set(15, value)
@property
def read_message_history(self):
"""Returns True if a user can read a text channel's previous messages."""
return self._bit(16)
@read_message_history.setter
def read_message_history(self, value):
self._set(16, value)
@property
def mention_everyone(self):
"""Returns True if a user's @everyone or @here will mention everyone in the text channel."""
return self._bit(17)
@mention_everyone.setter
def mention_everyone(self, value):
self._set(17, value)
@property
def external_emojis(self):
"""Returns True if a user can use emojis from other guilds."""
return self._bit(18)
@external_emojis.setter
def external_emojis(self, value):
self._set(18, value)
# 1 unused
@property
def connect(self):
"""Returns True if a user can connect to a voice channel."""
return self._bit(20)
@connect.setter
def connect(self, value):
self._set(20, value)
@property
def speak(self):
"""Returns True if a user can speak in a voice channel."""
return self._bit(21)
@speak.setter
def speak(self, value):
self._set(21, value)
@property
def mute_members(self):
"""Returns True if a user can mute other users."""
return self._bit(22)
@mute_members.setter
def mute_members(self, value):
self._set(22, value)
@property
def deafen_members(self):
"""Returns True if a user can deafen other users."""
return self._bit(23)
@deafen_members.setter
def deafen_members(self, value):
self._set(23, value)
@property
def move_members(self):
"""Returns True if a user can move users between other voice channels."""
return self._bit(24)
@move_members.setter
def move_members(self, value):
self._set(24, value)
@property
def use_voice_activation(self):
"""Returns True if a user can use voice activation in voice channels."""
return self._bit(25)
@use_voice_activation.setter
def use_voice_activation(self, value):
self._set(25, value)
@property
def change_nickname(self):
"""Returns True if a user can change their nickname in the guild."""
return self._bit(26)
@change_nickname.setter
def change_nickname(self, value):
self._set(26, value)
@property
def manage_nicknames(self):
"""Returns True if a user can change other user's nickname in the guild."""
return self._bit(27)
@manage_nicknames.setter
def manage_nicknames(self, value):
self._set(27, value)
@property
def manage_roles(self):
"""Returns True if a user can create or edit roles less than their role's position.
This also corresponds to the "Manage Permissions" channel-specific override.
"""
return self._bit(28)
@manage_roles.setter
def manage_roles(self, value):
self._set(28, value)
@property
def manage_webhooks(self):
"""Returns True if a user can create, edit, or delete webhooks."""
return self._bit(29)
@manage_webhooks.setter
def manage_webhooks(self, value):
self._set(29, value)
@property
def manage_emojis(self):
"""Returns True if a user can create, edit, or delete emojis."""
return self._bit(30)
@manage_emojis.setter
def manage_emojis(self, value):
self._set(30, value)
# 1 unused
# after these 32 bits, there's 21 more unused ones technically
def augment_from_permissions(cls):
cls.VALID_NAMES = {
name for name in dir(Permissions) if isinstance(getattr(Permissions, name), property)
}
# make descriptors for all the valid names
for name in cls.VALID_NAMES:
# god bless Python
def getter(self, x=name):
return self._values.get(x)
def setter(self, value, x=name):
self._set(x, value)
prop = property(getter, setter)
setattr(cls, name, prop)
return cls
@augment_from_permissions
class PermissionOverwrite:
r"""A type that is used to represent a channel specific permission.
Unlike a regular :class:`Permissions`\, the default value of a
permission is equivalent to ``None`` and not ``False``. Setting
a value to ``False`` is **explicitly** denying that permission,
while setting a value to ``True`` is **explicitly** allowing
that permission.
The values supported by this are the same as :class:`Permissions`
with the added possibility of it being set to ``None``.
Supported operations:
+-----------+------------------------------------------+
| Operation | Description |
+===========+==========================================+
| x == y | Checks if two overwrites are equal. |
+-----------+------------------------------------------+
| x != y | Checks if two overwrites are not equal. |
+-----------+------------------------------------------+
| iter(x) | Returns an iterator of (perm, value) |
| | pairs. This allows this class to be used |
| | as an iterable in e.g. set/list/dict |
| | constructions. |
+-----------+------------------------------------------+
Parameters
-----------
\*\*kwargs
Set the value of permissions by their name.
"""
__slots__ = ("_values",)
def __init__(self, **kwargs):
self._values = {}
for key, value in kwargs.items():
if key not in self.VALID_NAMES:
raise ValueError("no permission called {0}.".format(key))
setattr(self, key, value)
def __eq__(self, other):
return self._values == other._values
def _set(self, key, value):
if value not in (True, None, False):
raise TypeError(
"Expected bool or NoneType, received {0.__class__.__name__}".format(value)
)
self._values[key] = value
def pair(self):
"""Returns the (allow, deny) pair from this overwrite.
The value of these pairs is :class:`Permissions`.
"""
allow = Permissions.none()
deny = Permissions.none()
for key, value in self._values.items():
if value is True:
setattr(allow, key, True)
elif value is False:
setattr(deny, key, True)
return allow, deny
@classmethod
def from_pair(cls, allow, deny):
"""Creates an overwrite from an allow/deny pair of :class:`Permissions`."""
ret = cls()
for key, value in allow:
if value is True:
setattr(ret, key, True)
for key, value in deny:
if value is True:
setattr(ret, key, False)
return ret
def is_empty(self):
"""Checks if the permission overwrite is currently empty.
An empty permission overwrite is one that has no overwrites set
to True or False.
"""
return all(x is None for x in self._values.values())
def update(self, **kwargs):
r"""Bulk updates this permission overwrite object.
Allows you to set multiple attributes by using keyword
arguments. The names must be equivalent to the properties
listed. Extraneous key/value pairs will be silently ignored.
Parameters
------------
\*\*kwargs
A list of key/value pairs to bulk update with.
"""
for key, value in kwargs.items():
if key not in self.VALID_NAMES:
continue
setattr(self, key, value)
def __iter__(self):
for key in self.VALID_NAMES:
yield key, self._values.get(key)

369
discord/player.py Normal file
View File

@@ -0,0 +1,369 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import threading
import subprocess
import audioop
import asyncio
import logging
import shlex
import time
from .errors import ClientException
from .opus import Encoder as OpusEncoder
log = logging.getLogger(__name__)
__all__ = ["AudioSource", "PCMAudio", "FFmpegPCMAudio", "PCMVolumeTransformer"]
class AudioSource:
"""Represents an audio stream.
The audio stream can be Opus encoded or not, however if the audio stream
is not Opus encoded then the audio format must be 16-bit 48KHz stereo PCM.
.. warning::
The audio source reads are done in a separate thread.
"""
def read(self):
"""Reads 20ms worth of audio.
Subclasses must implement this.
If the audio is complete, then returning an empty
:term:`py:bytes-like object` to signal this is the way to do so.
If :meth:`is_opus` method returns ``True``, then it must return
20ms worth of Opus encoded audio. Otherwise, it must be 20ms
worth of 16-bit 48KHz stereo PCM, which is about 3,840 bytes
per frame (20ms worth of audio).
Returns
--------
bytes
A bytes like object that represents the PCM or Opus data.
"""
raise NotImplementedError
def is_opus(self):
"""Checks if the audio source is already encoded in Opus.
Defaults to ``False``.
"""
return False
def cleanup(self):
"""Called when clean-up is needed to be done.
Useful for clearing buffer data or processes after
it is done playing audio.
"""
pass
def __del__(self):
self.cleanup()
class PCMAudio(AudioSource):
"""Represents raw 16-bit 48KHz stereo PCM audio source.
Attributes
-----------
stream: file-like object
A file-like object that reads byte data representing raw PCM.
"""
def __init__(self, stream):
self.stream = stream
def read(self):
ret = self.stream.read(OpusEncoder.FRAME_SIZE)
if len(ret) != OpusEncoder.FRAME_SIZE:
return b""
return ret
class FFmpegPCMAudio(AudioSource):
"""An audio source from FFmpeg (or AVConv).
This launches a sub-process to a specific input file given.
.. warning::
You must have the ffmpeg or avconv executable in your path environment
variable in order for this to work.
Parameters
------------
source: Union[str, BinaryIO]
The input that ffmpeg will take and convert to PCM bytes.
If ``pipe`` is True then this is a file-like object that is
passed to the stdin of ffmpeg.
executable: str
The executable name (and path) to use. Defaults to ``ffmpeg``.
pipe: bool
If true, denotes that ``source`` parameter will be passed
to the stdin of ffmpeg. Defaults to ``False``.
stderr: Optional[BinaryIO]
A file-like object to pass to the Popen constructor.
Could also be an instance of ``subprocess.PIPE``.
options: Optional[str]
Extra command line arguments to pass to ffmpeg after the ``-i`` flag.
before_options: Optional[str]
Extra command line arguments to pass to ffmpeg before the ``-i`` flag.
Raises
--------
ClientException
The subprocess failed to be created.
"""
def __init__(
self,
source,
*,
executable="ffmpeg",
pipe=False,
stderr=None,
before_options=None,
options=None
):
stdin = None if not pipe else source
args = [executable]
if isinstance(before_options, str):
args.extend(shlex.split(before_options))
args.append("-i")
args.append("-" if pipe else source)
args.extend(("-f", "s16le", "-ar", "48000", "-ac", "2", "-loglevel", "warning"))
if isinstance(options, str):
args.extend(shlex.split(options))
args.append("pipe:1")
self._process = None
try:
self._process = subprocess.Popen(
args, stdin=stdin, stdout=subprocess.PIPE, stderr=stderr
)
self._stdout = self._process.stdout
except FileNotFoundError:
raise ClientException(executable + " was not found.") from None
except subprocess.SubprocessError as exc:
raise ClientException("Popen failed: {0.__class__.__name__}: {0}".format(exc)) from exc
def read(self):
ret = self._stdout.read(OpusEncoder.FRAME_SIZE)
if len(ret) != OpusEncoder.FRAME_SIZE:
return b""
return ret
def cleanup(self):
proc = self._process
if proc is None:
return
log.info("Preparing to terminate ffmpeg process %s.", proc.pid)
proc.kill()
if proc.poll() is None:
log.info("ffmpeg process %s has not terminated. Waiting to terminate...", proc.pid)
proc.communicate()
log.info(
"ffmpeg process %s should have terminated with a return code of %s.",
proc.pid,
proc.returncode,
)
else:
log.info(
"ffmpeg process %s successfully terminated with return code of %s.",
proc.pid,
proc.returncode,
)
self._process = None
class PCMVolumeTransformer(AudioSource):
"""Transforms a previous :class:`AudioSource` to have volume controls.
This does not work on audio sources that have :meth:`AudioSource.is_opus`
set to ``True``.
Parameters
------------
original: :class:`AudioSource`
The original AudioSource to transform.
volume: float
The initial volume to set it to.
See :attr:`volume` for more info.
Raises
-------
TypeError
Not an audio source.
ClientException
The audio source is opus encoded.
"""
def __init__(self, original, volume=1.0):
if not isinstance(original, AudioSource):
raise TypeError("expected AudioSource not {0.__class__.__name__}.".format(original))
if original.is_opus():
raise ClientException("AudioSource must not be Opus encoded.")
self.original = original
self.volume = volume
@property
def volume(self):
"""Retrieves or sets the volume as a floating point percentage (e.g. 1.0 for 100%)."""
return self._volume
@volume.setter
def volume(self, value):
self._volume = max(value, 0.0)
def cleanup(self):
self.original.cleanup()
def read(self):
ret = self.original.read()
return audioop.mul(ret, 2, min(self._volume, 2.0))
class AudioPlayer(threading.Thread):
DELAY = OpusEncoder.FRAME_LENGTH / 1000.0
def __init__(self, source, client, *, after=None):
threading.Thread.__init__(self)
self.daemon = True
self.source = source
self.client = client
self.after = after
self._end = threading.Event()
self._resumed = threading.Event()
self._resumed.set() # we are not paused
self._current_error = None
self._connected = client._connected
self._lock = threading.Lock()
if after is not None and not callable(after):
raise TypeError('Expected a callable for the "after" parameter.')
def _do_run(self):
self.loops = 0
self._start = time.time()
# getattr lookup speed ups
play_audio = self.client.send_audio_packet
self._speak(True)
while not self._end.is_set():
# are we paused?
if not self._resumed.is_set():
# wait until we aren't
self._resumed.wait()
continue
# are we disconnected from voice?
if not self._connected.is_set():
# wait until we are connected
self._connected.wait()
# reset our internal data
self.loops = 0
self._start = time.time()
self.loops += 1
data = self.source.read()
if not data:
self.stop()
break
play_audio(data, encode=not self.source.is_opus())
next_time = self._start + self.DELAY * self.loops
delay = max(0, self.DELAY + (next_time - time.time()))
time.sleep(delay)
def run(self):
try:
self._do_run()
except Exception as exc:
self._current_error = exc
self.stop()
finally:
self.source.cleanup()
self._call_after()
def _call_after(self):
if self.after is not None:
try:
self.after(self._current_error)
except Exception:
log.exception("Calling the after function failed.")
def stop(self):
self._end.set()
self._resumed.set()
self._speak(False)
def pause(self, *, update_speaking=True):
self._resumed.clear()
if update_speaking:
self._speak(False)
def resume(self, *, update_speaking=True):
self.loops = 0
self._start = time.time()
self._resumed.set()
if update_speaking:
self._speak(True)
def is_playing(self):
return self._resumed.is_set() and not self._end.is_set()
def is_paused(self):
return not self._end.is_set() and not self._resumed.is_set()
def _set_source(self, source):
with self._lock:
self.pause(update_speaking=False)
self.source = source
self.resume(update_speaking=False)
def _speak(self, speaking):
try:
asyncio.run_coroutine_threadsafe(self.client.ws.speak(speaking), self.client.loop)
except Exception as e:
log.info("Speaking call in player failed: %s", e)

151
discord/raw_models.py Normal file
View File

@@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
class RawMessageDeleteEvent:
"""Represents the event payload for a :func:`on_raw_message_delete` event.
Attributes
------------
channel_id: :class:`int`
The channel ID where the deletion took place.
guild_id: Optional[:class:`int`]
The guild ID where the deletion took place, if applicable.
message_id: :class:`int`
The message ID that got deleted.
"""
__slots__ = ("message_id", "channel_id", "guild_id")
def __init__(self, data):
self.message_id = int(data["id"])
self.channel_id = int(data["channel_id"])
try:
self.guild_id = int(data["guild_id"])
except KeyError:
self.guild_id = None
class RawBulkMessageDeleteEvent:
"""Represents the event payload for a :func:`on_raw_bulk_message_delete` event.
Attributes
-----------
message_ids: Set[:class:`int`]
A :class:`set` of the message IDs that were deleted.
channel_id: :class:`int`
The channel ID where the message got deleted.
guild_id: Optional[:class:`int`]
The guild ID where the message got deleted, if applicable.
"""
__slots__ = ("message_ids", "channel_id", "guild_id")
def __init__(self, data):
self.message_ids = {int(x) for x in data.get("ids", [])}
self.channel_id = int(data["channel_id"])
try:
self.guild_id = int(data["guild_id"])
except KeyError:
self.guild_id = None
class RawMessageUpdateEvent:
"""Represents the payload for a :func:`on_raw_message_edit` event.
Attributes
-----------
message_id: :class:`int`
The message ID that got updated.
data: :class:`dict`
The raw data given by the
`gateway <https://discordapp.com/developers/docs/topics/gateway#message-update>`_
"""
__slots__ = ("message_id", "data")
def __init__(self, data):
self.message_id = int(data["id"])
self.data = data
class RawReactionActionEvent:
"""Represents the payload for a :func:`on_raw_reaction_add` or
:func:`on_raw_reaction_remove` event.
Attributes
-----------
message_id: :class:`int`
The message ID that got or lost a reaction.
user_id: :class:`int`
The user ID who added the reaction or whose reaction was removed.
channel_id: :class:`int`
The channel ID where the reaction got added or removed.
guild_id: Optional[:class:`int`]
The guild ID where the reaction got added or removed, if applicable.
emoji: :class:`PartialEmoji`
The custom or unicode emoji being used.
"""
__slots__ = ("message_id", "user_id", "channel_id", "guild_id", "emoji")
def __init__(self, data, emoji):
self.message_id = int(data["message_id"])
self.channel_id = int(data["channel_id"])
self.user_id = int(data["user_id"])
self.emoji = emoji
try:
self.guild_id = int(data["guild_id"])
except KeyError:
self.guild_id = None
class RawReactionClearEvent:
"""Represents the payload for a :func:`on_raw_reaction_clear` event.
Attributes
-----------
message_id: :class:`int`
The message ID that got its reactions cleared.
channel_id: :class:`int`
The channel ID where the reactions got cleared.
guild_id: Optional[:class:`int`]
The guild ID where the reactions got cleared.
"""
__slots__ = ("message_id", "channel_id", "guild_id")
def __init__(self, data):
self.message_id = int(data["message_id"])
self.channel_id = int(data["channel_id"])
try:
self.guild_id = int(data["guild_id"])
except KeyError:
self.guild_id = None

151
discord/reaction.py Normal file
View File

@@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from .iterators import ReactionIterator
class Reaction:
"""Represents a reaction to a message.
Depending on the way this object was created, some of the attributes can
have a value of ``None``.
.. container:: operations
.. describe:: x == y
Checks if two reactions are equal. This works by checking if the emoji
is the same. So two messages with the same reaction will be considered
"equal".
.. describe:: x != y
Checks if two reactions are not equal.
.. describe:: hash(x)
Returns the reaction's hash.
.. describe:: str(x)
Returns the string form of the reaction's emoji.
Attributes
-----------
emoji: :class:`Emoji` or :class:`str`
The reaction emoji. May be a custom emoji, or a unicode emoji.
count: :class:`int`
Number of times this reaction was made
me: :class:`bool`
If the user sent this reaction.
message: :class:`Message`
Message this reaction is for.
"""
__slots__ = ("message", "count", "emoji", "me")
def __init__(self, *, message, data, emoji=None):
self.message = message
self.emoji = emoji or message._state.get_reaction_emoji(data["emoji"])
self.count = data.get("count", 1)
self.me = data.get("me")
@property
def custom_emoji(self):
""":class:`bool`: If this is a custom emoji."""
return not isinstance(self.emoji, str)
def __eq__(self, other):
return isinstance(other, self.__class__) and other.emoji == self.emoji
def __ne__(self, other):
if isinstance(other, self.__class__):
return other.emoji != self.emoji
return True
def __hash__(self):
return hash(self.emoji)
def __str__(self):
return str(self.emoji)
def __repr__(self):
return "<Reaction emoji={0.emoji!r} me={0.me} count={0.count}>".format(self)
def users(self, limit=None, after=None):
"""Returns an :class:`AsyncIterator` representing the users that have reacted to the message.
The ``after`` parameter must represent a member
and meet the :class:`abc.Snowflake` abc.
Parameters
------------
limit: int
The maximum number of results to return.
If not provided, returns all the users who
reacted to the message.
after: :class:`abc.Snowflake`
For pagination, reactions are sorted by member.
Raises
--------
HTTPException
Getting the users for the reaction failed.
Examples
---------
Usage ::
# I do not actually recommend doing this.
async for user in reaction.users():
await channel.send('{0} has reacted with {1.emoji}!'.format(user, reaction))
Flattening into a list: ::
users = await reaction.users().flatten()
# users is now a list...
winner = random.choice(users)
await channel.send('{} has won the raffle.'.format(winner))
Yields
--------
Union[:class:`User`, :class:`Member`]
The member (if retrievable) or the user that has reacted
to this message. The case where it can be a :class:`Member` is
in a guild message context. Sometimes it can be a :class:`User`
if the member has left the guild.
"""
if self.custom_emoji:
emoji = "{0.name}:{0.id}".format(self.emoji)
else:
emoji = self.emoji
if limit is None:
limit = self.count
return ReactionIterator(self.message, emoji, limit, after)

79
discord/relationship.py Normal file
View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from .enums import RelationshipType, try_enum
class Relationship:
"""Represents a relationship in Discord.
A relationship is like a friendship, a person who is blocked, etc.
Only non-bot accounts can have relationships.
Attributes
-----------
user: :class:`User`
The user you have the relationship with.
type: :class:`RelationshipType`
The type of relationship you have.
"""
__slots__ = ("type", "user", "_state")
def __init__(self, *, state, data):
self._state = state
self.type = try_enum(RelationshipType, data["type"])
self.user = state.store_user(data["user"])
def __repr__(self):
return "<Relationship user={0.user!r} type={0.type!r}>".format(self)
async def delete(self):
"""|coro|
Deletes the relationship.
Raises
------
HTTPException
Deleting the relationship failed.
"""
await self._state.http.remove_relationship(self.user.id)
async def accept(self):
"""|coro|
Accepts the relationship request. e.g. accepting a
friend request.
Raises
-------
HTTPException
Accepting the relationship failed.
"""
await self._state.http.add_relationship(self.user.id)

297
discord/role.py Normal file
View File

@@ -0,0 +1,297 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from .permissions import Permissions
from .errors import InvalidArgument
from .colour import Colour
from .mixins import Hashable
from .utils import snowflake_time
class Role(Hashable):
"""Represents a Discord role in a :class:`Guild`.
.. container:: operations
.. describe:: x == y
Checks if two roles are equal.
.. describe:: x != y
Checks if two roles are not equal.
.. describe:: x > y
Checks if a role is higher than another in the hierarchy.
.. describe:: x < y
Checks if a role is lower than another in the hierarchy.
.. describe:: x >= y
Checks if a role is higher or equal to another in the hierarchy.
.. describe:: x <= y
Checks if a role is lower or equal to another in the hierarchy.
.. describe:: hash(x)
Return the role's hash.
.. describe:: str(x)
Returns the role's name.
Attributes
----------
id: :class:`int`
The ID for the role.
name: :class:`str`
The name of the role.
permissions: :class:`Permissions`
Represents the role's permissions.
guild: :class:`Guild`
The guild the role belongs to.
colour: :class:`Colour`
Represents the role colour. An alias exists under ``color``.
hoist: :class:`bool`
Indicates if the role will be displayed separately from other members.
position: :class:`int`
The position of the role. This number is usually positive. The bottom
role has a position of 0.
managed: :class:`bool`
Indicates if the role is managed by the guild through some form of
integrations such as Twitch.
mentionable: :class:`bool`
Indicates if the role can be mentioned by users.
"""
__slots__ = (
"id",
"name",
"permissions",
"color",
"colour",
"position",
"managed",
"mentionable",
"hoist",
"guild",
"_state",
)
def __init__(self, *, guild, state, data):
self.guild = guild
self._state = state
self.id = int(data["id"])
self._update(data)
def __str__(self):
return self.name
def __repr__(self):
return "<Role id={0.id} name={0.name!r}>".format(self)
def __lt__(self, other):
if not isinstance(other, Role) or not isinstance(self, Role):
return NotImplemented
if self.guild != other.guild:
raise RuntimeError("cannot compare roles from two different guilds.")
# the @everyone role is always the lowest role in hierarchy
guild_id = self.guild.id
if self.id == guild_id:
# everyone_role < everyone_role -> False
return other.id != guild_id
if self.position < other.position:
return True
if self.position == other.position:
return int(self.id) > int(other.id)
return False
def __le__(self, other):
r = Role.__lt__(other, self)
if r is NotImplemented:
return NotImplemented
return not r
def __gt__(self, other):
return Role.__lt__(other, self)
def __ge__(self, other):
r = Role.__lt__(self, other)
if r is NotImplemented:
return NotImplemented
return not r
def _update(self, data):
self.name = data["name"]
self.permissions = Permissions(data.get("permissions", 0))
self.position = data.get("position", 0)
self.colour = Colour(data.get("color", 0))
self.hoist = data.get("hoist", False)
self.managed = data.get("managed", False)
self.mentionable = data.get("mentionable", False)
self.color = self.colour
def is_default(self):
"""Checks if the role is the default role."""
return self.guild.id == self.id
@property
def created_at(self):
"""Returns the role's creation time in UTC."""
return snowflake_time(self.id)
@property
def mention(self):
"""Returns a string that allows you to mention a role."""
return "<@&%s>" % self.id
@property
def members(self):
"""Returns a :class:`list` of :class:`Member` with this role."""
all_members = self.guild.members
if self.is_default():
return all_members
role_id = self.id
return [member for member in all_members if member._roles.has(role_id)]
async def _move(self, position, reason):
if position <= 0:
raise InvalidArgument("Cannot move role to position 0 or below")
if self.is_default():
raise InvalidArgument("Cannot move default role")
if self.position == position:
return # Save discord the extra request.
http = self._state.http
change_range = range(min(self.position, position), max(self.position, position) + 1)
roles = [
r.id for r in self.guild.roles[1:] if r.position in change_range and r.id != self.id
]
if self.position > position:
roles.insert(0, self.id)
else:
roles.append(self.id)
payload = [{"id": z[0], "position": z[1]} for z in zip(roles, change_range)]
await http.move_role_position(self.guild.id, payload, reason=reason)
async def edit(self, *, reason=None, **fields):
"""|coro|
Edits the role.
You must have the :attr:`~Permissions.manage_roles` permission to
use this.
All fields are optional.
Parameters
-----------
name: str
The new role name to change to.
permissions: :class:`Permissions`
The new permissions to change to.
colour: :class:`Colour`
The new colour to change to. (aliased to color as well)
hoist: bool
Indicates if the role should be shown separately in the member list.
mentionable: bool
Indicates if the role should be mentionable by others.
position: int
The new role's position. This must be below your top role's
position or it will fail.
reason: Optional[str]
The reason for editing this role. Shows up on the audit log.
Raises
-------
Forbidden
You do not have permissions to change the role.
HTTPException
Editing the role failed.
InvalidArgument
An invalid position was given or the default
role was asked to be moved.
"""
position = fields.get("position")
if position is not None:
await self._move(position, reason=reason)
self.position = position
try:
colour = fields["colour"]
except KeyError:
colour = fields.get("color", self.colour)
payload = {
"name": fields.get("name", self.name),
"permissions": fields.get("permissions", self.permissions).value,
"color": colour.value,
"hoist": fields.get("hoist", self.hoist),
"mentionable": fields.get("mentionable", self.mentionable),
}
data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload)
self._update(data)
async def delete(self, *, reason=None):
"""|coro|
Deletes the role.
You must have the :attr:`~Permissions.manage_roles` permission to
use this.
Parameters
-----------
reason: Optional[str]
The reason for deleting this role. Shows up on the audit log.
Raises
--------
Forbidden
You do not have permissions to delete the role.
HTTPException
Deleting the role failed.
"""
await self._state.http.delete_role(self.guild.id, self.id, reason=reason)

370
discord/shard.py Normal file
View File

@@ -0,0 +1,370 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import asyncio
import itertools
import logging
import websockets
from .state import AutoShardedConnectionState
from .client import Client
from .gateway import *
from .errors import ClientException, InvalidArgument
from . import utils
from .enums import Status
log = logging.getLogger(__name__)
class Shard:
def __init__(self, ws, client):
self.ws = ws
self._client = client
self.loop = self._client.loop
self._current = self.loop.create_future()
self._current.set_result(None) # we just need an already done future
self._pending = asyncio.Event(loop=self.loop)
self._pending_task = None
@property
def id(self):
return self.ws.shard_id
def is_pending(self):
return not self._pending.is_set()
def complete_pending_reads(self):
self._pending.set()
async def _pending_reads(self):
try:
while self.is_pending():
await self.poll()
except asyncio.CancelledError:
pass
def launch_pending_reads(self):
self._pending_task = asyncio.ensure_future(self._pending_reads(), loop=self.loop)
def wait(self):
return self._pending_task
async def poll(self):
try:
await self.ws.poll_event()
except ResumeWebSocket:
log.info("Got a request to RESUME the websocket at Shard ID %s.", self.id)
coro = DiscordWebSocket.from_client(
self._client,
resume=True,
shard_id=self.id,
session=self.ws.session_id,
sequence=self.ws.sequence,
)
self.ws = await asyncio.wait_for(coro, timeout=180.0, loop=self.loop)
def get_future(self):
if self._current.done():
self._current = asyncio.ensure_future(self.poll(), loop=self.loop)
return self._current
class AutoShardedClient(Client):
"""A client similar to :class:`Client` except it handles the complications
of sharding for the user into a more manageable and transparent single
process bot.
When using this client, you will be able to use it as-if it was a regular
:class:`Client` with a single shard when implementation wise internally it
is split up into multiple shards. This allows you to not have to deal with
IPC or other complicated infrastructure.
It is recommended to use this client only if you have surpassed at least
1000 guilds.
If no :attr:`shard_count` is provided, then the library will use the
Bot Gateway endpoint call to figure out how many shards to use.
If a ``shard_ids`` parameter is given, then those shard IDs will be used
to launch the internal shards. Note that :attr:`shard_count` must be provided
if this is used. By default, when omitted, the client will launch shards from
0 to ``shard_count - 1``.
Attributes
------------
shard_ids: Optional[List[:class:`int`]]
An optional list of shard_ids to launch the shards with.
"""
def __init__(self, *args, loop=None, **kwargs):
kwargs.pop("shard_id", None)
self.shard_ids = kwargs.pop("shard_ids", None)
super().__init__(*args, loop=loop, **kwargs)
if self.shard_ids is not None:
if self.shard_count is None:
raise ClientException(
"When passing manual shard_ids, you must provide a shard_count."
)
elif not isinstance(self.shard_ids, (list, tuple)):
raise ClientException("shard_ids parameter must be a list or a tuple.")
self._connection = AutoShardedConnectionState(
dispatch=self.dispatch,
chunker=self._chunker,
handlers=self._handlers,
syncer=self._syncer,
http=self.http,
loop=self.loop,
**kwargs
)
# instead of a single websocket, we have multiple
# the key is the shard_id
self.shards = {}
def _get_websocket(guild_id):
i = (guild_id >> 22) % self.shard_count
return self.shards[i].ws
self._connection._get_websocket = _get_websocket
async def _chunker(self, guild, *, shard_id=None):
try:
guild_id = guild.id
shard_id = shard_id or guild.shard_id
except AttributeError:
guild_id = [s.id for s in guild]
payload = {"op": 8, "d": {"guild_id": guild_id, "query": "", "limit": 0}}
ws = self.shards[shard_id].ws
await ws.send_as_json(payload)
@property
def latency(self):
""":class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds.
This operates similarly to :meth:`.Client.latency` except it uses the average
latency of every shard's latency. To get a list of shard latency, check the
:attr:`latencies` property. Returns ``nan`` if there are no shards ready.
"""
if not self.shards:
return float("nan")
return sum(latency for _, latency in self.latencies) / len(self.shards)
@property
def latencies(self):
"""List[Tuple[:class:`int`, :class:`float`]]: A list of latencies between a HEARTBEAT and a HEARTBEAT_ACK in seconds.
This returns a list of tuples with elements ``(shard_id, latency)``.
"""
return [(shard_id, shard.ws.latency) for shard_id, shard in self.shards.items()]
async def request_offline_members(self, *guilds):
r"""|coro|
Requests previously offline members from the guild to be filled up
into the :attr:`Guild.members` cache. This function is usually not
called. It should only be used if you have the ``fetch_offline_members``
parameter set to ``False``.
When the client logs on and connects to the websocket, Discord does
not provide the library with offline members if the number of members
in the guild is larger than 250. You can check if a guild is large
if :attr:`Guild.large` is ``True``.
Parameters
-----------
\*guilds
An argument list of guilds to request offline members for.
Raises
-------
InvalidArgument
If any guild is unavailable or not large in the collection.
"""
if any(not g.large or g.unavailable for g in guilds):
raise InvalidArgument("An unavailable or non-large guild was passed.")
_guilds = sorted(guilds, key=lambda g: g.shard_id)
for shard_id, sub_guilds in itertools.groupby(_guilds, key=lambda g: g.shard_id):
sub_guilds = list(sub_guilds)
await self._connection.request_offline_members(sub_guilds, shard_id=shard_id)
async def launch_shard(self, gateway, shard_id):
try:
coro = websockets.connect(
gateway, loop=self.loop, klass=DiscordWebSocket, compression=None
)
ws = await asyncio.wait_for(coro, loop=self.loop, timeout=180.0)
except Exception:
log.info("Failed to connect for shard_id: %s. Retrying...", shard_id)
await asyncio.sleep(5.0, loop=self.loop)
return await self.launch_shard(gateway, shard_id)
ws.token = self.http.token
ws._connection = self._connection
ws._dispatch = self.dispatch
ws.gateway = gateway
ws.shard_id = shard_id
ws.shard_count = self.shard_count
ws._max_heartbeat_timeout = self._connection.heartbeat_timeout
try:
# OP HELLO
await asyncio.wait_for(ws.poll_event(), loop=self.loop, timeout=180.0)
await asyncio.wait_for(ws.identify(), loop=self.loop, timeout=180.0)
except asyncio.TimeoutError:
log.info("Timed out when connecting for shard_id: %s. Retrying...", shard_id)
await asyncio.sleep(5.0, loop=self.loop)
return await self.launch_shard(gateway, shard_id)
# keep reading the shard while others connect
self.shards[shard_id] = ret = Shard(ws, self)
ret.launch_pending_reads()
await asyncio.sleep(5.0, loop=self.loop)
async def launch_shards(self):
if self.shard_count is None:
self.shard_count, gateway = await self.http.get_bot_gateway()
else:
gateway = await self.http.get_gateway()
self._connection.shard_count = self.shard_count
shard_ids = self.shard_ids if self.shard_ids else range(self.shard_count)
for shard_id in shard_ids:
await self.launch_shard(gateway, shard_id)
shards_to_wait_for = []
for shard in self.shards.values():
shard.complete_pending_reads()
shards_to_wait_for.append(shard.wait())
# wait for all pending tasks to finish
await utils.sane_wait_for(shards_to_wait_for, timeout=300.0, loop=self.loop)
async def _connect(self):
await self.launch_shards()
while True:
pollers = [shard.get_future() for shard in self.shards.values()]
done, _ = await asyncio.wait(
pollers, loop=self.loop, return_when=asyncio.FIRST_COMPLETED
)
for f in done:
# we wanna re-raise to the main Client.connect handler if applicable
f.result()
async def close(self):
"""|coro|
Closes the connection to discord.
"""
if self.is_closed():
return
self._closed.set()
for vc in self.voice_clients:
try:
await vc.disconnect()
except Exception:
pass
to_close = [shard.ws.close() for shard in self.shards.values()]
if to_close:
await asyncio.wait(to_close, loop=self.loop)
await self.http.close()
async def change_presence(self, *, activity=None, status=None, afk=False, shard_id=None):
"""|coro|
Changes the client's presence.
The activity parameter is a :class:`Activity` object (not a string) that represents
the activity being done currently. This could also be the slimmed down versions,
:class:`Game` and :class:`Streaming`.
Example: ::
game = discord.Game("with the API")
await client.change_presence(status=discord.Status.idle, activity=game)
Parameters
----------
activity: Optional[Union[:class:`Game`, :class:`Streaming`, :class:`Activity`]]
The activity being done. ``None`` if no currently active activity is done.
status: Optional[:class:`Status`]
Indicates what status to change to. If None, then
:attr:`Status.online` is used.
afk: bool
Indicates if you are going AFK. This allows the discord
client to know how to handle push notifications better
for you in case you are actually idle and not lying.
shard_id: Optional[int]
The shard_id to change the presence to. If not specified
or ``None``, then it will change the presence of every
shard the bot can see.
Raises
------
InvalidArgument
If the ``activity`` parameter is not of proper type.
"""
if status is None:
status = "online"
status_enum = Status.online
elif status is Status.offline:
status = "invisible"
status_enum = Status.offline
else:
status_enum = status
status = str(status)
if shard_id is None:
for shard in self.shards.values():
await shard.ws.change_presence(activity=activity, status=status, afk=afk)
guilds = self._connection.guilds
else:
shard = self.shards[shard_id]
await shard.ws.change_presence(activity=activity, status=status, afk=afk)
guilds = [g for g in self._connection.guilds if g.shard_id == shard_id]
for guild in guilds:
me = guild.me
if me is None:
continue
me.activities = (activity,)
me.status = status_enum

1048
discord/state.py Normal file

File diff suppressed because it is too large Load Diff

699
discord/user.py Normal file
View File

@@ -0,0 +1,699 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2017 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from collections import namedtuple
import discord.abc
from .utils import snowflake_time, _bytes_to_base64_data, parse_time, valid_icon_size
from .enums import DefaultAvatar, RelationshipType, UserFlags, HypeSquadHouse
from .errors import ClientException, InvalidArgument
from .colour import Colour
VALID_STATIC_FORMATS = {"jpeg", "jpg", "webp", "png"}
VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"}
class Profile(namedtuple("Profile", "flags user mutual_guilds connected_accounts premium_since")):
__slots__ = ()
@property
def nitro(self):
return self.premium_since is not None
premium = nitro
def _has_flag(self, o):
v = o.value
return (self.flags & v) == v
@property
def staff(self):
return self._has_flag(UserFlags.staff)
@property
def partner(self):
return self._has_flag(UserFlags.partner)
@property
def bug_hunter(self):
return self._has_flag(UserFlags.bug_hunter)
@property
def early_supporter(self):
return self._has_flag(UserFlags.early_supporter)
@property
def hypesquad(self):
return self._has_flag(UserFlags.hypesquad)
@property
def hypesquad_houses(self):
flags = (
UserFlags.hypesquad_bravery,
UserFlags.hypesquad_brilliance,
UserFlags.hypesquad_balance,
)
return [house for house, flag in zip(HypeSquadHouse, flags) if self._has_flag(flag)]
_BaseUser = discord.abc.User
class BaseUser(_BaseUser):
__slots__ = ("name", "id", "discriminator", "avatar", "bot", "_state")
def __init__(self, *, state, data):
self._state = state
self.name = data["username"]
self.id = int(data["id"])
self.discriminator = data["discriminator"]
self.avatar = data["avatar"]
self.bot = data.get("bot", False)
def __str__(self):
return "{0.name}#{0.discriminator}".format(self)
def __eq__(self, other):
return isinstance(other, _BaseUser) and other.id == self.id
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return self.id >> 22
@classmethod
def _copy(cls, user):
self = cls.__new__(cls) # bypass __init__
self.name = user.name
self.id = user.id
self.discriminator = user.discriminator
self.avatar = user.avatar
self.bot = user.bot
self._state = user._state
return self
@property
def avatar_url(self):
"""Returns a friendly URL version of the avatar the user has.
If the user does not have a traditional avatar, their default
avatar URL is returned instead.
This is equivalent to calling :meth:`avatar_url_as` with
the default parameters (i.e. webp/gif detection and a size of 1024).
"""
return self.avatar_url_as(format=None, size=1024)
def is_avatar_animated(self):
""":class:`bool`: Returns True if the user has an animated avatar."""
return bool(self.avatar and self.avatar.startswith("a_"))
def avatar_url_as(self, *, format=None, static_format="webp", size=1024):
"""Returns a friendly URL version of the avatar the user has.
If the user does not have a traditional avatar, their default
avatar URL is returned instead.
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif', and
'gif' is only valid for animated avatars. The size must be a power of 2
between 16 and 1024.
Parameters
-----------
format: Optional[str]
The format to attempt to convert the avatar to.
If the format is ``None``, then it is automatically
detected into either 'gif' or static_format depending on the
avatar being animated or not.
static_format: 'str'
Format to attempt to convert only non-animated avatars to.
Defaults to 'webp'
size: int
The size of the image to display.
Returns
--------
str
The resulting CDN URL.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or ``static_format``, or
invalid ``size``.
"""
if not valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 1024")
if format is not None and format not in VALID_AVATAR_FORMATS:
raise InvalidArgument("format must be None or one of {}".format(VALID_AVATAR_FORMATS))
if format == "gif" and not self.is_avatar_animated():
raise InvalidArgument("non animated avatars do not support gif format")
if static_format not in VALID_STATIC_FORMATS:
raise InvalidArgument("static_format must be one of {}".format(VALID_STATIC_FORMATS))
if self.avatar is None:
return self.default_avatar_url
if format is None:
if self.is_avatar_animated():
format = "gif"
else:
format = static_format
return "https://cdn.discordapp.com/avatars/{0.id}/{0.avatar}.{1}?size={2}".format(
self, format, size
)
@property
def default_avatar(self):
"""Returns the default avatar for a given user. This is calculated by the user's discriminator"""
return DefaultAvatar(int(self.discriminator) % len(DefaultAvatar))
@property
def default_avatar_url(self):
"""Returns a URL for a user's default avatar."""
return "https://cdn.discordapp.com/embed/avatars/{}.png".format(self.default_avatar.value)
@property
def colour(self):
"""A property that returns a :class:`Colour` denoting the rendered colour
for the user. This always returns :meth:`Colour.default`.
There is an alias for this under ``color``.
"""
return Colour.default()
color = colour
@property
def mention(self):
"""Returns a string that allows you to mention the given user."""
return "<@{0.id}>".format(self)
def permissions_in(self, channel):
"""An alias for :meth:`abc.GuildChannel.permissions_for`.
Basically equivalent to:
.. code-block:: python3
channel.permissions_for(self)
Parameters
-----------
channel
The channel to check your permissions for.
"""
return channel.permissions_for(self)
@property
def created_at(self):
"""Returns the user's creation time in UTC.
This is when the user's discord account was created."""
return snowflake_time(self.id)
@property
def display_name(self):
"""Returns the user's display name.
For regular users this is just their username, but
if they have a guild specific nickname then that
is returned instead.
"""
return self.name
def mentioned_in(self, message):
"""Checks if the user is mentioned in the specified message.
Parameters
-----------
message : :class:`Message`
The message to check if you're mentioned in.
"""
if message.mention_everyone:
return True
for user in message.mentions:
if user.id == self.id:
return True
return False
class ClientUser(BaseUser):
"""Represents your Discord user.
.. container:: operations
.. describe:: x == y
Checks if two users are equal.
.. describe:: x != y
Checks if two users are not equal.
.. describe:: hash(x)
Return the user's hash.
.. describe:: str(x)
Returns the user's name with discriminator.
Attributes
-----------
name: :class:`str`
The user's username.
id: :class:`int`
The user's unique ID.
discriminator: :class:`str`
The user's discriminator. This is given when the username has conflicts.
avatar: Optional[:class:`str`]
The avatar hash the user has. Could be None.
bot: :class:`bool`
Specifies if the user is a bot account.
verified: :class:`bool`
Specifies if the user is a verified account.
email: Optional[:class:`str`]
The email the user used when registering.
mfa_enabled: :class:`bool`
Specifies if the user has MFA turned on and working.
premium: :class:`bool`
Specifies if the user is a premium user (e.g. has Discord Nitro).
"""
__slots__ = ("email", "verified", "mfa_enabled", "premium", "_relationships")
def __init__(self, *, state, data):
super().__init__(state=state, data=data)
self.verified = data.get("verified", False)
self.email = data.get("email")
self.mfa_enabled = data.get("mfa_enabled", False)
self.premium = data.get("premium", False)
self._relationships = {}
def __repr__(self):
return (
"<ClientUser id={0.id} name={0.name!r} discriminator={0.discriminator!r}"
" bot={0.bot} verified={0.verified} mfa_enabled={0.mfa_enabled}>".format(self)
)
def get_relationship(self, user_id):
"""Retrieves the :class:`Relationship` if applicable.
Parameters
-----------
user_id: int
The user ID to check if we have a relationship with them.
Returns
--------
Optional[:class:`Relationship`]
The relationship if available or ``None``
"""
return self._relationships.get(user_id)
@property
def relationships(self):
"""Returns a :class:`list` of :class:`Relationship` that the user has."""
return list(self._relationships.values())
@property
def friends(self):
r"""Returns a :class:`list` of :class:`User`\s that the user is friends with."""
return [r.user for r in self._relationships.values() if r.type is RelationshipType.friend]
@property
def blocked(self):
r"""Returns a :class:`list` of :class:`User`\s that the user has blocked."""
return [r.user for r in self._relationships.values() if r.type is RelationshipType.blocked]
async def edit(self, **fields):
"""|coro|
Edits the current profile of the client.
If a bot account is used then a password field is optional,
otherwise it is required.
Note
-----
To upload an avatar, a :term:`py:bytes-like object` must be passed in that
represents the image being uploaded. If this is done through a file
then the file must be opened via ``open('some_filename', 'rb')`` and
the :term:`py:bytes-like object` is given through the use of ``fp.read()``.
The only image formats supported for uploading is JPEG and PNG.
Parameters
-----------
password : str
The current password for the client's account.
Only applicable to user accounts.
new_password: str
The new password you wish to change to.
Only applicable to user accounts.
email: str
The new email you wish to change to.
Only applicable to user accounts.
house: Optional[:class:`HypeSquadHouse`]
The hypesquad house you wish to change to.
Could be ``None`` to leave the current house.
Only applicable to user accounts.
username :str
The new username you wish to change to.
avatar: bytes
A :term:`py:bytes-like object` representing the image to upload.
Could be ``None`` to denote no avatar.
Raises
------
HTTPException
Editing your profile failed.
InvalidArgument
Wrong image format passed for ``avatar``.
ClientException
Password is required for non-bot accounts.
House field was not a HypeSquadHouse.
"""
try:
avatar_bytes = fields["avatar"]
except KeyError:
avatar = self.avatar
else:
if avatar_bytes is not None:
avatar = _bytes_to_base64_data(avatar_bytes)
else:
avatar = None
not_bot_account = not self.bot
password = fields.get("password")
if not_bot_account and password is None:
raise ClientException("Password is required for non-bot accounts.")
args = {
"password": password,
"username": fields.get("username", self.name),
"avatar": avatar,
}
if not_bot_account:
args["email"] = fields.get("email", self.email)
if "new_password" in fields:
args["new_password"] = fields["new_password"]
http = self._state.http
if "house" in fields:
house = fields["house"]
if house is None:
await http.leave_hypesquad_house()
elif not isinstance(house, HypeSquadHouse):
raise ClientException("`house` parameter was not a HypeSquadHouse")
else:
value = house.value
await http.change_hypesquad_house(value)
data = await http.edit_profile(**args)
if not_bot_account:
self.email = data["email"]
try:
http._token(data["token"], bot=False)
except KeyError:
pass
# manually update data by calling __init__ explicitly.
self.__init__(state=self._state, data=data)
async def create_group(self, *recipients):
r"""|coro|
Creates a group direct message with the recipients
provided. These recipients must be have a relationship
of type :attr:`RelationshipType.friend`.
Bot accounts cannot create a group.
Parameters
-----------
\*recipients
An argument :class:`list` of :class:`User` to have in
your group.
Return
-------
:class:`GroupChannel`
The new group channel.
Raises
-------
HTTPException
Failed to create the group direct message.
ClientException
Attempted to create a group with only one recipient.
This does not include yourself.
"""
from .channel import GroupChannel
if len(recipients) < 2:
raise ClientException("You must have two or more recipients to create a group.")
users = [str(u.id) for u in recipients]
data = await self._state.http.start_group(self.id, users)
return GroupChannel(me=self, data=data, state=self._state)
class User(BaseUser, discord.abc.Messageable):
"""Represents a Discord user.
.. container:: operations
.. describe:: x == y
Checks if two users are equal.
.. describe:: x != y
Checks if two users are not equal.
.. describe:: hash(x)
Return the user's hash.
.. describe:: str(x)
Returns the user's name with discriminator.
Attributes
-----------
name: :class:`str`
The user's username.
id: :class:`int`
The user's unique ID.
discriminator: :class:`str`
The user's discriminator. This is given when the username has conflicts.
avatar: Optional[:class:`str`]
The avatar hash the user has. Could be None.
bot: :class:`bool`
Specifies if the user is a bot account.
"""
__slots__ = ("__weakref__",)
def __repr__(self):
return "<User id={0.id} name={0.name!r} discriminator={0.discriminator!r} bot={0.bot}>".format(
self
)
async def _get_channel(self):
ch = await self.create_dm()
return ch
@property
def dm_channel(self):
"""Returns the :class:`DMChannel` associated with this user if it exists.
If this returns ``None``, you can create a DM channel by calling the
:meth:`create_dm` coroutine function.
"""
return self._state._get_private_channel_by_user(self.id)
async def create_dm(self):
"""Creates a :class:`DMChannel` with this user.
This should be rarely called, as this is done transparently for most
people.
"""
found = self.dm_channel
if found is not None:
return found
state = self._state
data = await state.http.start_private_message(self.id)
return state.add_dm_channel(data)
@property
def relationship(self):
"""Returns the :class:`Relationship` with this user if applicable, ``None`` otherwise."""
return self._state.user.get_relationship(self.id)
async def mutual_friends(self):
"""|coro|
Gets all mutual friends of this user. This can only be used by non-bot accounts
Returns
-------
List[:class:`User`]
The users that are mutual friends.
Raises
-------
Forbidden
Not allowed to get mutual friends of this user.
HTTPException
Getting mutual friends failed.
"""
state = self._state
mutuals = await state.http.get_mutual_friends(self.id)
return [User(state=state, data=friend) for friend in mutuals]
def is_friend(self):
""":class:`bool`: Checks if the user is your friend."""
r = self.relationship
if r is None:
return False
return r.type is RelationshipType.friend
def is_blocked(self):
""":class:`bool`: Checks if the user is blocked."""
r = self.relationship
if r is None:
return False
return r.type is RelationshipType.blocked
async def block(self):
"""|coro|
Blocks the user.
Raises
-------
Forbidden
Not allowed to block this user.
HTTPException
Blocking the user failed.
"""
await self._state.http.add_relationship(self.id, type=RelationshipType.blocked.value)
async def unblock(self):
"""|coro|
Unblocks the user.
Raises
-------
Forbidden
Not allowed to unblock this user.
HTTPException
Unblocking the user failed.
"""
await self._state.http.remove_relationship(self.id)
async def remove_friend(self):
"""|coro|
Removes the user as a friend.
Raises
-------
Forbidden
Not allowed to remove this user as a friend.
HTTPException
Removing the user as a friend failed.
"""
await self._state.http.remove_relationship(self.id)
async def send_friend_request(self):
"""|coro|
Sends the user a friend request.
Raises
-------
Forbidden
Not allowed to send a friend request to the user.
HTTPException
Sending the friend request failed.
"""
await self._state.http.send_friend_request(
username=self.name, discriminator=self.discriminator
)
async def profile(self):
"""|coro|
Gets the user's profile. This can only be used by non-bot accounts.
Raises
-------
Forbidden
Not allowed to fetch profiles.
HTTPException
Fetching the profile failed.
Returns
--------
:class:`Profile`
The profile of the user.
"""
state = self._state
data = await state.http.get_user_profile(self.id)
def transform(d):
return state._get_guild(int(d["id"]))
since = data.get("premium_since")
mutual_guilds = list(filter(None, map(transform, data.get("mutual_guilds", []))))
return Profile(
flags=data["user"].get("flags", 0),
premium_since=parse_time(since),
mutual_guilds=mutual_guilds,
user=self,
connected_accounts=data["connected_accounts"],
)

369
discord/utils.py Normal file
View File

@@ -0,0 +1,369 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import array
import asyncio
import unicodedata
from base64 import b64encode
from bisect import bisect_left
import datetime
from email.utils import parsedate_to_datetime
import functools
from inspect import isawaitable as _isawaitable
import json
import re
import warnings
from .errors import InvalidArgument
DISCORD_EPOCH = 1420070400000
class cached_property:
def __init__(self, function):
self.function = function
self.__doc__ = getattr(function, "__doc__")
def __get__(self, instance, owner):
if instance is None:
return self
value = self.function(instance)
setattr(instance, self.function.__name__, value)
return value
class CachedSlotProperty:
def __init__(self, name, function):
self.name = name
self.function = function
self.__doc__ = getattr(function, "__doc__")
def __get__(self, instance, owner):
if instance is None:
return self
try:
return getattr(instance, self.name)
except AttributeError:
value = self.function(instance)
setattr(instance, self.name, value)
return value
def cached_slot_property(name):
def decorator(func):
return CachedSlotProperty(name, func)
return decorator
def parse_time(timestamp):
if timestamp:
return datetime.datetime(*map(int, re.split(r"[^\d]", timestamp.replace("+00:00", ""))))
return None
def deprecated(instead=None):
def actual_decorator(func):
@functools.wraps(func)
def decorated(*args, **kwargs):
warnings.simplefilter("always", DeprecationWarning) # turn off filter
if instead:
fmt = "{0.__name__} is deprecated, use {1} instead."
else:
fmt = "{0.__name__} is deprecated."
warnings.warn(fmt.format(func, instead), stacklevel=3, category=DeprecationWarning)
warnings.simplefilter("default", DeprecationWarning) # reset filter
return func(*args, **kwargs)
return decorated
return actual_decorator
def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None):
"""A helper function that returns the OAuth2 URL for inviting the bot
into guilds.
Parameters
-----------
client_id : str
The client ID for your bot.
permissions : :class:`Permissions`
The permissions you're requesting. If not given then you won't be requesting any
permissions.
guild : :class:`Guild`
The guild to pre-select in the authorization screen, if available.
redirect_uri : str
An optional valid redirect URI.
"""
url = "https://discordapp.com/oauth2/authorize?client_id={}&scope=bot".format(client_id)
if permissions is not None:
url = url + "&permissions=" + str(permissions.value)
if guild is not None:
url = url + "&guild_id=" + str(guild.id)
if redirect_uri is not None:
from urllib.parse import urlencode
url = url + "&response_type=code&" + urlencode({"redirect_uri": redirect_uri})
return url
def snowflake_time(id):
"""Returns the creation date in UTC of a discord id."""
return datetime.datetime.utcfromtimestamp(((id >> 22) + DISCORD_EPOCH) / 1000)
def time_snowflake(datetime_obj, high=False):
"""Returns a numeric snowflake pretending to be created at the given date.
When using as the lower end of a range, use time_snowflake(high=False) - 1 to be inclusive, high=True to be exclusive
When using as the higher end of a range, use time_snowflake(high=True) + 1 to be inclusive, high=False to be exclusive
Parameters
-----------
datetime_obj
A timezone-naive datetime object representing UTC time.
high
Whether or not to set the lower 22 bit to high or low.
"""
unix_seconds = (datetime_obj - type(datetime_obj)(1970, 1, 1)).total_seconds()
discord_millis = int(unix_seconds * 1000 - DISCORD_EPOCH)
return (discord_millis << 22) + (2 ** 22 - 1 if high else 0)
def find(predicate, seq):
"""A helper to return the first element found in the sequence
that meets the predicate. For example: ::
member = find(lambda m: m.name == 'Mighty', channel.guild.members)
would find the first :class:`Member` whose name is 'Mighty' and return it.
If an entry is not found, then ``None`` is returned.
This is different from `filter`_ due to the fact it stops the moment it finds
a valid entry.
.. _filter: https://docs.python.org/3.6/library/functions.html#filter
Parameters
-----------
predicate
A function that returns a boolean-like result.
seq : iterable
The iterable to search through.
"""
for element in seq:
if predicate(element):
return element
return None
def get(iterable, **attrs):
r"""A helper that returns the first element in the iterable that meets
all the traits passed in ``attrs``. This is an alternative for
:func:`discord.utils.find`.
When multiple attributes are specified, they are checked using
logical AND, not logical OR. Meaning they have to meet every
attribute passed in and not one of them.
To have a nested attribute search (i.e. search by ``x.y``) then
pass in ``x__y`` as the keyword argument.
If nothing is found that matches the attributes passed, then
``None`` is returned.
Examples
---------
Basic usage:
.. code-block:: python3
member = discord.utils.get(message.guild.members, name='Foo')
Multiple attribute matching:
.. code-block:: python3
channel = discord.utils.get(guild.voice_channels, name='Foo', bitrate=64000)
Nested attribute matching:
.. code-block:: python3
channel = discord.utils.get(client.get_all_channels(), guild__name='Cool', name='general')
Parameters
-----------
iterable
An iterable to search through.
\*\*attrs
Keyword arguments that denote attributes to search with.
"""
def predicate(elem):
for attr, val in attrs.items():
nested = attr.split("__")
obj = elem
for attribute in nested:
obj = getattr(obj, attribute)
if obj != val:
return False
return True
return find(predicate, iterable)
def _unique(iterable):
seen = set()
adder = seen.add
return [x for x in iterable if not (x in seen or adder(x))]
def _get_as_snowflake(data, key):
try:
value = data[key]
except KeyError:
return None
else:
return value and int(value)
def _get_mime_type_for_image(data):
if data.startswith(b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"):
return "image/png"
elif data.startswith(b"\xFF\xD8") and data.rstrip(b"\0").endswith(b"\xFF\xD9"):
return "image/jpeg"
elif data.startswith((b"\x47\x49\x46\x38\x37\x61", b"\x47\x49\x46\x38\x39\x61")):
return "image/gif"
elif data.startswith(b"RIFF") and data[8:12] == b"WEBP":
return "image/webp"
else:
raise InvalidArgument("Unsupported image type given")
def _bytes_to_base64_data(data):
fmt = "data:{mime};base64,{data}"
mime = _get_mime_type_for_image(data)
b64 = b64encode(data).decode("ascii")
return fmt.format(mime=mime, data=b64)
def to_json(obj):
return json.dumps(obj, separators=(",", ":"), ensure_ascii=True)
def _parse_ratelimit_header(request):
now = parsedate_to_datetime(request.headers["Date"])
reset = datetime.datetime.fromtimestamp(
int(request.headers["X-Ratelimit-Reset"]), datetime.timezone.utc
)
return (reset - now).total_seconds()
async def maybe_coroutine(f, *args, **kwargs):
value = f(*args, **kwargs)
if _isawaitable(value):
return await value
else:
return value
async def async_all(gen, *, check=_isawaitable):
for elem in gen:
if check(elem):
elem = await elem
if not elem:
return False
return True
async def sane_wait_for(futures, *, timeout, loop):
_, pending = await asyncio.wait(futures, timeout=timeout, loop=loop)
if len(pending) != 0:
raise asyncio.TimeoutError()
def valid_icon_size(size):
"""Icons must be power of 2 within [16, 2048]."""
return not size & (size - 1) and size in range(16, 2049)
class SnowflakeList(array.array):
"""Internal data storage class to efficiently store a list of snowflakes.
This should have the following characteristics:
- Low memory usage
- O(n) iteration (obviously)
- O(n log n) initial creation if data is unsorted
- O(log n) search and indexing
- O(n) insertion
"""
__slots__ = ()
def __new__(cls, data, *, is_sorted=False):
return array.array.__new__(cls, "Q", data if is_sorted else sorted(data))
def add(self, element):
i = bisect_left(self, element)
self.insert(i, element)
def get(self, element):
i = bisect_left(self, element)
return self[i] if i != len(self) and self[i] == element else None
def has(self, element):
i = bisect_left(self, element)
return i != len(self) and self[i] == element
_IS_ASCII = re.compile(r"^[\x00-\x7f]+$")
def _string_width(string, *, _IS_ASCII=_IS_ASCII):
"""Returns string's width."""
match = _IS_ASCII.match(string)
if match:
return match.endpos
UNICODE_WIDE_CHAR_TYPE = "WFA"
width = 0
func = unicodedata.east_asian_width
for char in string:
width += 2 if func(char) in UNICODE_WIDE_CHAR_TYPE else 1
return width

448
discord/voice_client.py Normal file
View File

@@ -0,0 +1,448 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
"""Some documentation to refer to:
- Our main web socket (mWS) sends opcode 4 with a guild ID and channel ID.
- The mWS receives VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE.
- We pull the session_id from VOICE_STATE_UPDATE.
- We pull the token, endpoint and server_id from VOICE_SERVER_UPDATE.
- Then we initiate the voice web socket (vWS) pointing to the endpoint.
- We send opcode 0 with the user_id, server_id, session_id and token using the vWS.
- The vWS sends back opcode 2 with an ssrc, port, modes(array) and hearbeat_interval.
- We send a UDP discovery packet to endpoint:port and receive our IP and our port in LE.
- Then we send our IP and port via vWS with opcode 1.
- When that's all done, we receive opcode 4 from the vWS.
- Finally we can transmit data to endpoint:port.
"""
import asyncio
import socket
import logging
import struct
import threading
from . import opus
from .backoff import ExponentialBackoff
from .gateway import *
from .errors import ClientException, ConnectionClosed
from .player import AudioPlayer, AudioSource
try:
import nacl.secret
has_nacl = True
except ImportError:
has_nacl = False
log = logging.getLogger(__name__)
class VoiceClient:
"""Represents a Discord voice connection.
You do not create these, you typically get them from
e.g. :meth:`VoiceChannel.connect`.
Warning
--------
In order to play audio, you must have loaded the opus library
through :func:`opus.load_opus`.
If you don't do this then the library will not be able to
transmit audio.
Attributes
-----------
session_id: :class:`str`
The voice connection session ID.
token: :class:`str`
The voice connection token.
endpoint: :class:`str`
The endpoint we are connecting to.
channel: :class:`abc.Connectable`
The voice channel connected to.
loop
The event loop that the voice client is running on.
"""
def __init__(self, state, timeout, channel):
if not has_nacl:
raise RuntimeError("PyNaCl library needed in order to use voice")
self.channel = channel
self.main_ws = None
self.timeout = timeout
self.ws = None
self.socket = None
self.loop = state.loop
self._state = state
# this will be used in the AudioPlayer thread
self._connected = threading.Event()
self._handshake_complete = asyncio.Event(loop=self.loop)
self.mode = None
self._connections = 0
self.sequence = 0
self.timestamp = 0
self._runner = None
self._player = None
self.encoder = opus.Encoder()
warn_nacl = not has_nacl
supported_modes = ("xsalsa20_poly1305_suffix", "xsalsa20_poly1305")
@property
def guild(self):
"""Optional[:class:`Guild`]: The guild we're connected to, if applicable."""
return getattr(self.channel, "guild", None)
@property
def user(self):
""":class:`ClientUser`: The user connected to voice (i.e. ourselves)."""
return self._state.user
def checked_add(self, attr, value, limit):
val = getattr(self, attr)
if val + value > limit:
setattr(self, attr, 0)
else:
setattr(self, attr, val + value)
# connection related
async def start_handshake(self):
log.info("Starting voice handshake...")
guild_id, channel_id = self.channel._get_voice_state_pair()
state = self._state
self.main_ws = ws = state._get_websocket(guild_id)
self._connections += 1
# request joining
await ws.voice_state(guild_id, channel_id)
try:
await asyncio.wait_for(
self._handshake_complete.wait(), timeout=self.timeout, loop=self.loop
)
except asyncio.TimeoutError:
await self.terminate_handshake(remove=True)
raise
log.info(
"Voice handshake complete. Endpoint found %s (IP: %s)", self.endpoint, self.endpoint_ip
)
async def terminate_handshake(self, *, remove=False):
guild_id, channel_id = self.channel._get_voice_state_pair()
self._handshake_complete.clear()
await self.main_ws.voice_state(guild_id, None, self_mute=True)
log.info(
"The voice handshake is being terminated for Channel ID %s (Guild ID %s)",
channel_id,
guild_id,
)
if remove:
log.info(
"The voice client has been removed for Channel ID %s (Guild ID %s)",
channel_id,
guild_id,
)
key_id, _ = self.channel._get_voice_client_key()
self._state._remove_voice_client(key_id)
async def _create_socket(self, server_id, data):
self._connected.clear()
self.session_id = self.main_ws.session_id
self.server_id = server_id
self.token = data.get("token")
endpoint = data.get("endpoint")
if endpoint is None or self.token is None:
log.warning(
"Awaiting endpoint... This requires waiting. "
"If timeout occurred considering raising the timeout and reconnecting."
)
return
self.endpoint = endpoint.replace(":80", "")
self.endpoint_ip = socket.gethostbyname(self.endpoint)
if self.socket:
try:
self.socket.close()
except Exception:
pass
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.socket.setblocking(False)
if self._handshake_complete.is_set():
# terminate the websocket and handle the reconnect loop if necessary.
self._handshake_complete.clear()
await self.ws.close(4000)
return
self._handshake_complete.set()
async def connect(self, *, reconnect=True, _tries=0, do_handshake=True):
log.info("Connecting to voice...")
try:
del self.secret_key
except AttributeError:
pass
if do_handshake:
await self.start_handshake()
try:
self.ws = await DiscordVoiceWebSocket.from_client(self)
self._connected.clear()
while not hasattr(self, "secret_key"):
await self.ws.poll_event()
self._connected.set()
except (ConnectionClosed, asyncio.TimeoutError):
if reconnect and _tries < 5:
log.exception("Failed to connect to voice... Retrying...")
await asyncio.sleep(1 + _tries * 2.0, loop=self.loop)
await self.terminate_handshake()
await self.connect(reconnect=reconnect, _tries=_tries + 1)
else:
raise
if self._runner is None:
self._runner = self.loop.create_task(self.poll_voice_ws(reconnect))
async def poll_voice_ws(self, reconnect):
backoff = ExponentialBackoff()
while True:
try:
await self.ws.poll_event()
except (ConnectionClosed, asyncio.TimeoutError) as exc:
if isinstance(exc, ConnectionClosed):
if exc.code == 1000:
await self.disconnect()
break
if not reconnect:
await self.disconnect()
raise
retry = backoff.delay()
log.exception("Disconnected from voice... Reconnecting in %.2fs.", retry)
self._connected.clear()
await asyncio.sleep(retry, loop=self.loop)
await self.terminate_handshake()
try:
await self.connect(reconnect=True)
except asyncio.TimeoutError:
# at this point we've retried 5 times... let's continue the loop.
log.warning("Could not connect to voice... Retrying...")
continue
async def disconnect(self, *, force=False):
"""|coro|
Disconnects this voice client from voice.
"""
if not force and not self._connected.is_set():
return
self.stop()
self._connected.clear()
try:
if self.ws:
await self.ws.close()
await self.terminate_handshake(remove=True)
finally:
if self.socket:
self.socket.close()
async def move_to(self, channel):
"""|coro|
Moves you to a different voice channel.
Parameters
-----------
channel: :class:`abc.Snowflake`
The channel to move to. Must be a voice channel.
"""
guild_id, _ = self.channel._get_voice_state_pair()
await self.main_ws.voice_state(guild_id, channel.id)
def is_connected(self):
""":class:`bool`: Indicates if the voice client is connected to voice."""
return self._connected.is_set()
# audio related
def _get_voice_packet(self, data):
header = bytearray(12)
# Formulate rtp header
header[0] = 0x80
header[1] = 0x78
struct.pack_into(">H", header, 2, self.sequence)
struct.pack_into(">I", header, 4, self.timestamp)
struct.pack_into(">I", header, 8, self.ssrc)
encrypt_packet = getattr(self, "_encrypt_" + self.mode)
return encrypt_packet(header, data)
def _encrypt_xsalsa20_poly1305(self, header, data):
box = nacl.secret.SecretBox(bytes(self.secret_key))
nonce = bytearray(24)
nonce[:12] = header
return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext
def _encrypt_xsalsa20_poly1305_suffix(self, header, data):
box = nacl.secret.SecretBox(bytes(self.secret_key))
nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE)
return header + box.encrypt(bytes(data), nonce).ciphertext + nonce
def play(self, source, *, after=None):
"""Plays an :class:`AudioSource`.
The finalizer, ``after`` is called after the source has been exhausted
or an error occurred.
If an error happens while the audio player is running, the exception is
caught and the audio player is then stopped.
Parameters
-----------
source: :class:`AudioSource`
The audio source we're reading from.
after
The finalizer that is called after the stream is exhausted.
All exceptions it throws are silently discarded. This function
must have a single parameter, ``error``, that denotes an
optional exception that was raised during playing.
Raises
-------
ClientException
Already playing audio or not connected.
TypeError
source is not a :class:`AudioSource` or after is not a callable.
"""
if not self._connected:
raise ClientException("Not connected to voice.")
if self.is_playing():
raise ClientException("Already playing audio.")
if not isinstance(source, AudioSource):
raise TypeError("source must an AudioSource not {0.__class__.__name__}".format(source))
self._player = AudioPlayer(source, self, after=after)
self._player.start()
def is_playing(self):
"""Indicates if we're currently playing audio."""
return self._player is not None and self._player.is_playing()
def is_paused(self):
"""Indicates if we're playing audio, but if we're paused."""
return self._player is not None and self._player.is_paused()
def stop(self):
"""Stops playing audio."""
if self._player:
self._player.stop()
self._player = None
def pause(self):
"""Pauses the audio playing."""
if self._player:
self._player.pause()
def resume(self):
"""Resumes the audio playing."""
if self._player:
self._player.resume()
@property
def source(self):
"""Optional[:class:`AudioSource`]: The audio source being played, if playing.
This property can also be used to change the audio source currently being played.
"""
return self._player.source if self._player else None
@source.setter
def source(self, value):
if not isinstance(value, AudioSource):
raise TypeError("expected AudioSource not {0.__class__.__name__}.".format(value))
if self._player is None:
raise ValueError("Not playing anything.")
self._player._set_source(value)
def send_audio_packet(self, data, *, encode=True):
"""Sends an audio packet composed of the data.
You must be connected to play audio.
Parameters
----------
data: bytes
The :term:`py:bytes-like object` denoting PCM or Opus voice data.
encode: bool
Indicates if ``data`` should be encoded into Opus.
Raises
-------
ClientException
You are not connected.
OpusError
Encoding the data failed.
"""
self.checked_add("sequence", 1, 65535)
if encode:
encoded_data = self.encoder.encode(data, self.encoder.SAMPLES_PER_FRAME)
else:
encoded_data = data
packet = self._get_voice_packet(encoded_data)
try:
self.socket.sendto(packet, (self.endpoint_ip, self.voice_port))
except BlockingIOError:
log.warning(
"A packet has been dropped (seq: %s, timestamp: %s)", self.sequence, self.timestamp
)
self.checked_add("timestamp", self.encoder.SAMPLES_PER_FRAME, 4294967295)

703
discord/webhook.py Normal file
View File

@@ -0,0 +1,703 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2019 Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import asyncio
import json
import time
import re
import aiohttp
from . import utils
from .errors import InvalidArgument, HTTPException, Forbidden, NotFound
from .user import BaseUser, User
__all__ = ["WebhookAdapter", "AsyncWebhookAdapter", "RequestsWebhookAdapter", "Webhook"]
class WebhookAdapter:
"""Base class for all webhook adapters.
Attributes
------------
webhook: :class:`Webhook`
The webhook that owns this adapter.
"""
BASE = "https://discordapp.com/api/v7"
def _prepare(self, webhook):
self._webhook_id = webhook.id
self._webhook_token = webhook.token
self._request_url = "{0.BASE}/webhooks/{1}/{2}".format(self, webhook.id, webhook.token)
self.webhook = webhook
def request(self, verb, url, payload=None, multipart=None):
"""Actually does the request.
Subclasses must implement this.
Parameters
-----------
verb: str
The HTTP verb to use for the request.
url: str
The URL to send the request to. This will have
the query parameters already added to it, if any.
multipart: Optional[dict]
A dict containing multipart form data to send with
the request. If a filename is being uploaded, then it will
be under a ``file`` key which will have a 3-element :class:`tuple`
denoting ``(filename, file, content_type)``.
payload: Optional[dict]
The JSON to send with the request, if any.
"""
raise NotImplementedError()
def delete_webhook(self):
return self.request("DELETE", self._request_url)
def edit_webhook(self, **payload):
return self.request("PATCH", self._request_url, payload=payload)
def handle_execution_response(self, data, *, wait):
"""Transforms the webhook execution response into something
more meaningful.
This is mainly used to convert the data into a :class:`Message`
if necessary.
Subclasses must implement this.
Parameters
------------
data
The data that was returned from the request.
wait: bool
Whether the webhook execution was asked to wait or not.
"""
raise NotImplementedError()
def store_user(self, data):
# mocks a ConnectionState for appropriate use for Message
return BaseUser(state=self.webhook._state, data=data)
def execute_webhook(self, *, payload, wait=False, file=None, files=None):
if file is not None:
multipart = {"file": file, "payload_json": utils.to_json(payload)}
data = None
elif files is not None:
multipart = {"payload_json": utils.to_json(payload)}
for i, file in enumerate(files, start=1):
multipart["file%i" % i] = file
data = None
else:
data = payload
multipart = None
url = "%s?wait=%d" % (self._request_url, wait)
maybe_coro = self.request("POST", url, multipart=multipart, payload=data)
return self.handle_execution_response(maybe_coro, wait=wait)
class AsyncWebhookAdapter(WebhookAdapter):
"""A webhook adapter suited for use with aiohttp.
.. note::
You are responsible for cleaning up the client session.
Parameters
-----------
session: aiohttp.ClientSession
The session to use to send requests.
"""
def __init__(self, session):
self.session = session
self.loop = session.loop
async def request(self, verb, url, payload=None, multipart=None):
headers = {}
data = None
if payload:
headers["Content-Type"] = "application/json"
data = utils.to_json(payload)
if multipart:
data = aiohttp.FormData()
for key, value in multipart.items():
if key.startswith("file"):
data.add_field(key, value[1], filename=value[0], content_type=value[2])
else:
data.add_field(key, value)
for tries in range(5):
async with self.session.request(verb, url, headers=headers, data=data) as r:
data = await r.text(encoding="utf-8")
if r.headers["Content-Type"] == "application/json":
data = json.loads(data)
# check if we have rate limit header information
remaining = r.headers.get("X-Ratelimit-Remaining")
if remaining == "0" and r.status != 429:
delta = utils._parse_ratelimit_header(r)
await asyncio.sleep(delta, loop=self.loop)
if 300 > r.status >= 200:
return data
# we are being rate limited
if r.status == 429:
retry_after = data["retry_after"] / 1000.0
await asyncio.sleep(retry_after, loop=self.loop)
continue
if r.status in (500, 502):
await asyncio.sleep(1 + tries * 2, loop=self.loop)
continue
if r.status == 403:
raise Forbidden(r, data)
elif r.status == 404:
raise NotFound(r, data)
else:
raise HTTPException(r, data)
async def handle_execution_response(self, response, *, wait):
data = await response
if not wait:
return data
# transform into Message object
from .message import Message
return Message(data=data, state=self.webhook._state, channel=self.webhook.channel)
class RequestsWebhookAdapter(WebhookAdapter):
"""A webhook adapter suited for use with ``requests``.
Only versions of requests higher than 2.13.0 are supported.
Parameters
-----------
session: Optional[`requests.Session <http://docs.python-requests.org/en/latest/api/#requests.Session>`_]
The requests session to use for sending requests. If not given then
each request will create a new session. Note if a session is given,
the webhook adapter **will not** clean it up for you. You must close
the session yourself.
sleep: bool
Whether to sleep the thread when encountering a 429 or pre-emptive
rate limit or a 5xx status code. Defaults to ``True``. If set to
``False`` then this will raise an :exc:`HTTPException` instead.
"""
def __init__(self, session=None, *, sleep=True):
import requests
self.session = session or requests
self.sleep = sleep
def request(self, verb, url, payload=None, multipart=None):
headers = {}
data = None
if payload:
headers["Content-Type"] = "application/json"
data = utils.to_json(payload)
if multipart is not None:
data = {"payload_json": multipart.pop("payload_json")}
for tries in range(5):
r = self.session.request(verb, url, headers=headers, data=data, files=multipart)
r.encoding = "utf-8"
data = r.text
# compatibility with aiohttp
r.status = r.status_code
if r.headers["Content-Type"] == "application/json":
data = json.loads(data)
# check if we have rate limit header information
remaining = r.headers.get("X-Ratelimit-Remaining")
if remaining == "0" and r.status != 429 and self.sleep:
delta = utils._parse_ratelimit_header(r)
time.sleep(delta)
if 300 > r.status >= 200:
return data
# we are being rate limited
if r.status == 429:
if self.sleep:
retry_after = data["retry_after"] / 1000.0
time.sleep(retry_after)
continue
else:
raise HTTPException(r, data)
if self.sleep and r.status in (500, 502):
time.sleep(1 + tries * 2)
continue
if r.status == 403:
raise Forbidden(r, data)
elif r.status == 404:
raise NotFound(r, data)
else:
raise HTTPException(r, data)
def handle_execution_response(self, response, *, wait):
if not wait:
return response
# transform into Message object
from .message import Message
return Message(data=response, state=self.webhook._state, channel=self.webhook.channel)
class Webhook:
"""Represents a Discord webhook.
Webhooks are a form to send messages to channels in Discord without a
bot user or authentication.
There are two main ways to use Webhooks. The first is through the ones
received by the library such as :meth:`.Guild.webhooks` and
:meth:`.TextChannel.webhooks`. The ones received by the library will
automatically have an adapter bound using the library's HTTP session.
Those webhooks will have :meth:`~.Webhook.send`, :meth:`~.Webhook.delete` and
:meth:`~.Webhook.edit` as coroutines.
The second form involves creating a webhook object manually without having
it bound to a websocket connection using the :meth:`~.Webhook.from_url` or
:meth:`~.Webhook.partial` classmethods. This form allows finer grained control
over how requests are done, allowing you to mix async and sync code using either
``aiohttp`` or ``requests``.
For example, creating a webhook from a URL and using ``aiohttp``:
.. code-block:: python3
from discord import Webhook, AsyncWebhookAdapter
import aiohttp
async def foo():
async with aiohttp.ClientSession() as session:
webhook = Webhook.from_url('url-here', adapter=AsyncWebhookAdapter(session))
await webhook.send('Hello World', username='Foo')
Or creating a webhook from an ID and token and using ``requests``:
.. code-block:: python3
import requests
from discord import Webhook, RequestsWebhookAdapter
webhook = Webhook.partial(123456, 'abcdefg', adapter=RequestsWebhookAdapter())
webhook.send('Hello World', username='Foo')
Attributes
------------
id: :class:`int`
The webhook's ID
token: :class:`str`
The authentication token of the webhook.
guild_id: Optional[:class:`int`]
The guild ID this webhook is for.
channel_id: Optional[:class:`int`]
The channel ID this webhook is for.
user: Optional[:class:`abc.User`]
The user this webhook was created by. If the webhook was
received without authentication then this will be ``None``.
name: Optional[:class:`str`]
The default name of the webhook.
avatar: Optional[:class:`str`]
The default avatar of the webhook.
"""
__slots__ = (
"id",
"guild_id",
"channel_id",
"user",
"name",
"avatar",
"token",
"_state",
"_adapter",
)
def __init__(self, data, *, adapter, state=None):
self.id = int(data["id"])
self.channel_id = utils._get_as_snowflake(data, "channel_id")
self.guild_id = utils._get_as_snowflake(data, "guild_id")
self.name = data.get("name")
self.avatar = data.get("avatar")
self.token = data["token"]
self._state = state
self._adapter = adapter
self._adapter._prepare(self)
user = data.get("user")
if user is None:
self.user = None
elif state is None:
self.user = BaseUser(state=None, data=user)
else:
self.user = User(state=state, data=user)
def __repr__(self):
return "<Webhook id=%r>" % self.id
@property
def url(self):
"""Returns the webhook's url."""
return "https://discordapp.com/api/webhooks/{}/{}".format(self.id, self.token)
@classmethod
def partial(cls, id, token, *, adapter):
"""Creates a partial :class:`Webhook`.
A partial webhook is just a webhook object with an ID and a token.
Parameters
-----------
id: int
The ID of the webhook.
token: str
The authentication token of the webhook.
adapter: :class:`WebhookAdapter`
The webhook adapter to use when sending requests. This is
typically :class:`AsyncWebhookAdapter` for ``aiohttp`` or
:class:`RequestsWebhookAdapter` for ``requests``.
"""
if not isinstance(adapter, WebhookAdapter):
raise TypeError("adapter must be a subclass of WebhookAdapter")
data = {"id": id, "token": token}
return cls(data, adapter=adapter)
@classmethod
def from_url(cls, url, *, adapter):
"""Creates a partial :class:`Webhook` from a webhook URL.
Parameters
------------
url: str
The URL of the webhook.
adapter: :class:`WebhookAdapter`
The webhook adapter to use when sending requests. This is
typically :class:`AsyncWebhookAdapter` for ``aiohttp`` or
:class:`RequestsWebhookAdapter` for ``requests``.
Raises
-------
InvalidArgument
The URL is invalid.
"""
m = re.search(
r"discordapp.com/api/webhooks/(?P<id>[0-9]{17,21})/(?P<token>[A-Za-z0-9\.\-\_]{60,68})",
url,
)
if m is None:
raise InvalidArgument("Invalid webhook URL given.")
return cls(m.groupdict(), adapter=adapter)
@classmethod
def from_state(cls, data, state):
return cls(data, adapter=AsyncWebhookAdapter(session=state.http._session), state=state)
@property
def guild(self):
"""Optional[:class:`Guild`]: The guild this webhook belongs to.
If this is a partial webhook, then this will always return ``None``.
"""
return self._state and self._state._get_guild(self.guild_id)
@property
def channel(self):
"""Optional[:class:`TextChannel`]: The text channel this webhook belongs to.
If this is a partial webhook, then this will always return ``None``.
"""
guild = self.guild
return guild and guild.get_channel(self.channel_id)
@property
def created_at(self):
"""Returns the webhook's creation time in UTC."""
return utils.snowflake_time(self.id)
@property
def avatar_url(self):
"""Returns a friendly URL version of the avatar the webhook has.
If the webhook does not have a traditional avatar, their default
avatar URL is returned instead.
This is equivalent to calling :meth:`avatar_url_as` with the
default parameters.
"""
return self.avatar_url_as()
def avatar_url_as(self, *, format=None, size=1024):
"""Returns a friendly URL version of the avatar the webhook has.
If the webhook does not have a traditional avatar, their default
avatar URL is returned instead.
The format must be one of 'jpeg', 'jpg', or 'png'.
The size must be a power of 2 between 16 and 1024.
Parameters
-----------
format: Optional[str]
The format to attempt to convert the avatar to.
If the format is ``None``, then it is equivalent to png.
size: int
The size of the image to display.
Returns
--------
str
The resulting CDN URL.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
"""
if self.avatar is None:
# Default is always blurple apparently
return "https://cdn.discordapp.com/embed/avatars/0.png"
if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 1024")
format = format or "png"
if format not in ("png", "jpg", "jpeg"):
raise InvalidArgument("format must be one of 'png', 'jpg', or 'jpeg'.")
return "https://cdn.discordapp.com/avatars/{0.id}/{0.avatar}.{1}?size={2}".format(
self, format, size
)
def delete(self):
"""|maybecoro|
Deletes this Webhook.
If the webhook is constructed with a :class:`RequestsWebhookAdapter` then this is
not a coroutine.
Raises
-------
HTTPException
Deleting the webhook failed.
NotFound
This webhook does not exist.
Forbidden
You do not have permissions to delete this webhook.
"""
return self._adapter.delete_webhook()
def edit(self, **kwargs):
"""|maybecoro|
Edits this Webhook.
If the webhook is constructed with a :class:`RequestsWebhookAdapter` then this is
not a coroutine.
Parameters
-------------
name: Optional[str]
The webhook's new default name.
avatar: Optional[bytes]
A :term:`py:bytes-like object` representing the webhook's new default avatar.
Raises
-------
HTTPException
Editing the webhook failed.
NotFound
This webhook does not exist.
Forbidden
You do not have permissions to edit this webhook.
"""
payload = {}
try:
name = kwargs["name"]
except KeyError:
pass
else:
if name is not None:
payload["name"] = str(name)
else:
payload["name"] = None
try:
avatar = kwargs["avatar"]
except KeyError:
pass
else:
if avatar is not None:
payload["avatar"] = utils._bytes_to_base64_data(avatar)
else:
payload["avatar"] = None
return self._adapter.edit_webhook(**payload)
def send(
self,
content=None,
*,
wait=False,
username=None,
avatar_url=None,
tts=False,
file=None,
files=None,
embed=None,
embeds=None
):
"""|maybecoro|
Sends a message using the webhook.
If the webhook is constructed with a :class:`RequestsWebhookAdapter` then this is
not a coroutine.
The content must be a type that can convert to a string through ``str(content)``.
To upload a single file, the ``file`` parameter should be used with a
single :class:`File` object.
If the ``embed`` parameter is provided, it must be of type :class:`Embed` and
it must be a rich embed type. You cannot mix the ``embed`` parameter with the
``embeds`` parameter, which must be a :class:`list` of :class:`Embed` objects to send.
Parameters
------------
content
The content of the message to send.
wait: bool
Whether the server should wait before sending a response. This essentially
means that the return type of this function changes from ``None`` to
a :class:`Message` if set to ``True``.
username: str
The username to send with this message. If no username is provided
then the default username for the webhook is used.
avatar_url: str
The avatar URL to send with this message. If no avatar URL is provided
then the default avatar for the webhook is used.
tts: bool
Indicates if the message should be sent using text-to-speech.
file: :class:`File`
The file to upload. This cannot be mixed with ``files`` parameter.
files: List[:class:`File`]
A list of files to send with the content. This cannot be mixed with the
``file`` parameter.
embed: :class:`Embed`
The rich embed for the content to send. This cannot be mixed with
``embeds`` parameter.
embeds: List[:class:`Embed`]
A list of embeds to send with the content. Maximum of 10. This cannot
be mixed with the ``embed`` parameter.
Raises
--------
HTTPException
Sending the message failed.
NotFound
This webhook was not found.
Forbidden
The authorization token for the webhook is incorrect.
InvalidArgument
You specified both ``embed`` and ``embeds`` or the length of
``embeds`` was invalid.
Returns
---------
Optional[:class:`Message`]
The message that was sent.
"""
payload = {}
if files is not None and file is not None:
raise InvalidArgument("Cannot mix file and files keyword arguments.")
if embeds is not None and embed is not None:
raise InvalidArgument("Cannot mix embed and embeds keyword arguments.")
if embeds is not None:
if len(embeds) > 10:
raise InvalidArgument("embeds has a maximum of 10 elements.")
payload["embeds"] = [e.to_dict() for e in embeds]
if embed is not None:
payload["embeds"] = [embed.to_dict()]
if content is not None:
payload["content"] = str(content)
payload["tts"] = tts
if avatar_url:
payload["avatar_url"] = avatar_url
if username:
payload["username"] = username
if file is not None:
try:
to_pass = (file.filename, file.open_file(), "application/octet-stream")
return self._adapter.execute_webhook(wait=wait, file=to_pass, payload=payload)
finally:
file.close()
elif files is not None:
try:
to_pass = [
(file.filename, file.open_file(), "application/octet-stream") for file in files
]
return self._adapter.execute_webhook(wait=wait, files=to_pass, payload=payload)
finally:
for file in files:
file.close()
else:
return self._adapter.execute_webhook(wait=wait, payload=payload)
def execute(self, *args, **kwargs):
"""An alias for :meth:`~.Webhook.send`."""
return self.send(*args, **kwargs)

View File

@@ -0,0 +1,53 @@
.. systemd service guide
==============================================
Setting up auto-restart using systemd on Linux
==============================================
-------------------------
Creating the service file
-------------------------
Create the new service file:
:code:`sudo -e /etc/systemd/system/red@.service`
Paste the following and replace all instances of :code:`username` with the username your bot is running under (hopefully not root):
.. code-block:: none
[Unit]
Description=%I redbot
After=multi-user.target
[Service]
ExecStart=/home/username/.local/bin/redbot %I --no-prompt
User=username
Group=username
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 **@**
To start the bot, run the service and add the instance name after the **@**:
:code:`sudo systemctl start red@instancename`
To set the bot to start on boot, you must enable the service, again adding the instance name after the **@**:
:code:`sudo systemctl enable red@instancename`
To view Reds log, you can acccess through journalctl:
:code:`sudo journalctl -u red@instancename`

109
docs/cog_customcom.rst Normal file
View File

@@ -0,0 +1,109 @@
.. CustomCommands Cog Reference
============================
CustomCommands Cog Reference
============================
------------
How it works
------------
CustomCommands allows you to create simple commands for your bot without requiring you to code your own cog for Red.
If the command you attempt to create shares a name with an already loaded command, you cannot overwrite it with this cog.
---------
Cooldowns
---------
You can set cooldowns for your custom commands. If a command is on cooldown, it will not be triggered.
You can set cooldowns per member or per channel, or set a cooldown guild-wide. You can also set multiple types of cooldown on a single custom command. All cooldowns must pass before the command will trigger.
------------------
Context Parameters
------------------
You can enhance your custom command's response by leaving spaces for the bot to substitute.
+-----------+----------------------------------------+
| Argument | Substitute |
+===========+========================================+
| {message} | The message the bot is responding to. |
+-----------+----------------------------------------+
| {author} | The user who called the command. |
+-----------+----------------------------------------+
| {channel} | The channel the command was called in. |
+-----------+----------------------------------------+
| {server} | The server the command was called in. |
+-----------+----------------------------------------+
| {guild} | Same as with {server}. |
+-----------+----------------------------------------+
You can further refine the response with dot notation. For example, {author.mention} will mention the user who called the command.
------------------
Command Parameters
------------------
You can further enhance your custom command's response by leaving spaces for the user to substitute.
To do this, simply put {#} in the response, replacing # with any number starting with 0. Each number will be replaced with what the user gave the command, in order.
You can refine the response with colon notation. For example, {0:Member} will accept members of the server, and {0:int} will accept a number. If no colon notation is provided, the argument will be returned unchanged.
+-----------------+--------------------------------+
| Argument | Substitute |
+=================+================================+
| {#:Member} | A member of your server. |
+-----------------+--------------------------------+
| {#:TextChannel} | A text channel in your server. |
+-----------------+--------------------------------+
| {#:Role} | A role in your server. |
+-----------------+--------------------------------+
| {#:int} | A whole number. |
+-----------------+--------------------------------+
| {#:float} | A decimal number. |
+-----------------+--------------------------------+
| {#:bool} | True or False. |
+-----------------+--------------------------------+
You can specify more than the above with colon notation, but those are the most common.
As with context parameters, you can use dot notation to further refine the response. For example, {0.mention:Member} will mention the Member specified.
----------------
Example commands
----------------
Showing your own avatar
.. code-block:: none
[p]customcom add simple avatar {author.avatar_url}
[p]avatar
https://cdn.discordapp.com/avatars/133801473317404673/be4c4a4fe47cb3e74c31a0504e7a295e.webp?size=1024
Repeating the user
.. code-block:: none
[p]customcom add simple say {0}
[p]say Pete and Repeat
Pete and Repeat
Greeting the specified member
.. code-block:: none
[p]customcom add simple greet Hello, {0.mention:Member}!
[p]greet Twentysix
Hello, @Twentysix!
Comparing two text channel's categories
.. code-block:: none
[p]customcom add simple comparecategory {0.category:TextChannel} | {1.category:TextChannel}
[p]comparecategory #support #general
Red | Community

View File

@@ -0,0 +1,62 @@
.. Importing data from a V2 install
================================
Importing data from a V2 install
================================
----------------
What you'll need
----------------
1. A Running V3 bot
2. The path where your V2 bot is installed
--------------
Importing data
--------------
.. important::
Unless otherwise specified, the V2 data will take priority over V3 data for the same entires
.. important::
For the purposes of this guide, your prefix will be denoted as
[p]
You should swap whatever you made your prefix in for this.
All of the below are commands to be entered in discord where the bot can
see them.
The dataconverter cog is not loaded by default. To start, load it with
.. code-block:: none
[p]load dataconverter
Next, you'll need to give it the path where your V2 install is.
On linux and OSX, it may look something like:
.. code-block:: none
/home/username/Red-DiscordBot/
On Windows it will look something like:
.. code-block:: none
C:\Users\yourusername\Red-DiscordBot
Once you have that path, give it to the bot with the following command
(make sure to swap your own path in)
.. code-block:: none
[p]convertdata /home/username/Red-DiscordBot/
From here, if the path is correct, you will be prompted with an interactive menu asking you
what data you would like to import
You can select an entry by number, or quit with any of 'quit', 'exit', 'q', '-1', or 'cancel'

97
docs/cog_permissions.rst Normal file
View File

@@ -0,0 +1,97 @@
.. 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 normally.
Rules can also be added to cogs, which will affect all commands from that cog. The cog name can be
found from the help menu.
-------------
Rule priority
-------------
Rules set for subcommands will take precedence over rules set for the parent commands, which
lastly take precedence over rules set for the cog. So for example, if a user is denied the Core
cog, but allowed the ``[p]set token`` command, the user will not be able to use any command in the
Core cog except for ``[p]set token``.
In terms of scope, global rules will be checked first, then server rules.
For each of those, the first rule pertaining to one of the following models will be used:
1. User
2. Voice channel
3. Text channel
4. Channel category
5. Roles, highest to lowest
6. Server (can only be in global rules)
7. Default rules
In private messages, only global rules about a user will be checked.
-------------------------
Setting Rules From a File
-------------------------
The permissions cog can also set, display or update rules with a YAML file with the
``[p]permissions yaml`` command. Models must be represented by ID. Rules must be ``true`` for
allow, or ``false`` for deny. Here is an example:
.. code-block:: yaml
COG:
Admin:
78631113035100160: true
96733288462286848: false
Audio:
133049272517001216: true
default: false
COMMAND:
cleanup bot:
78631113035100160: true
default: false
ping:
96733288462286848: false
default: true
----------------------
Example configurations
----------------------
Locking the ``[p]play`` command to approved server(s) as a bot owner:
.. code-block:: none
[p]permissions setglobaldefault play deny
[p]permissions addglobalrule allow play [server ID or name]
Locking the ``[p]play`` command to specific voice channel(s) as a serverowner or admin:
.. code-block:: none
[p]permissions setserverdefault deny play
[p]permissions setserverdefault deny "playlist start"
[p]permissions addserverrule allow play [voice channel ID or name]
[p]permissions addserverrule allow "playlist start" [voice channel ID or name]
Allowing extra roles to use ``[p]cleanup``:
.. code-block:: none
[p]permissions addserverrule allow cleanup [role ID]
Preventing ``[p]cleanup`` from being used in channels where message history is important:
.. code-block:: none
[p]permissions addserverrule 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,38 +35,41 @@ 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",
"sphinx.ext.doctest",
"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 = '2017, 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 = '3.0.0a1'
version = __version__
# The full version, including alpha/beta/rc tags.
release = '3.0.0a1'
release = __version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -77,10 +81,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
@@ -94,7 +98,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
@@ -104,16 +108,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.
@@ -121,12 +125,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",
]
}
@@ -134,7 +138,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 ---------------------------------------------
@@ -143,15 +147,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',
@@ -161,8 +162,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")
]
@@ -170,10 +170,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 -------------------------------------------
@@ -182,15 +179,35 @@ 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",
)
]
# -- Options for linkcheck builder ----------------------------------------
# A list of regular expressions that match URIs that should not be
# checked when doing a linkcheck build.
linkcheck_ignore = [r"https://java.com*"]
# Example configuration for intersphinx: refer to the Python standard library.
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)}
# -- Options for extensions -----------------------------------------------
# Intersphinx
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),
}
# Doctest
# If this string is non-empty, all blocks with ``>>>`` in them will be
# tested, not just the ones explicitly marked with ``.. doctest::``
doctest_test_doctest_blocks = ""

23
docs/framework_bot.rst Normal file
View File

@@ -0,0 +1,23 @@
.. bot module docs
===
Bot
===
.. automodule:: redbot.core.bot
RedBase
^^^^^^^
.. autoclass:: RedBase
:members:
:exclude-members: get_context
.. automethod:: register_rpc_handler
.. automethod:: unregister_rpc_handler
Red
^^^
.. autoclass:: Red
:members:

11
docs/framework_checks.rst Normal file
View File

@@ -0,0 +1,11 @@
.. _checks:
========================
Command Check Decorators
========================
The following are all decorators for commands, which add restrictions to where and when they can be
run.
.. automodule:: redbot.core.checks
:members:

View File

@@ -0,0 +1,26 @@
.. red commands module documentation
================
Commands Package
================
This package acts almost identically to :doc:`discord.ext.commands <dpy:ext/commands/api>`; i.e.
all of the attributes from discord.py's are also in ours.
Some of these attributes, however, have been slightly modified, while others have been added to
extend functionlities used throughout the bot, as outlined below.
.. autofunction:: redbot.core.commands.command
.. autofunction:: redbot.core.commands.group
.. autoclass:: redbot.core.commands.Command
:members:
.. autoclass:: redbot.core.commands.Group
:members:
.. autoclass:: redbot.core.commands.Context
:members:
.. automodule:: redbot.core.commands.requires
:members: PrivilegeLevel, PermState, Requires

View File

@@ -29,7 +29,7 @@ Basic Usage
@commands.command()
async def return_some_data(self, ctx):
await ctx.send(await config.foo())
await ctx.send(await self.config.foo())
********
Tutorial
@@ -175,6 +175,195 @@ example, :py:meth:`Config.clear_all_guilds` resets all guild data. For member
data, you can clear on both a per-guild and guild-independent basis, see
:py:meth:`Config.clear_all_members` for more info.
**************
Advanced Usage
**************
Config makes it extremely easy to organize data that can easily fit into one of the standard categories (global,
guild, user etc.) but there may come a time when your data does not work with the existing categories. There are now
features within Config to enable developers to work with data how they wish.
This usage guide will cover the following features:
- :py:meth:`Group.get_raw`
- :py:meth:`Group.set_raw`
- :py:meth:`Group.clear_raw`
For this example let's suppose that we're creating a cog that allows users to buy and own multiple pets using
the built-in Economy credits::
from redbot.core import bank
from redbot.core import Config
from discord.ext import commands
class Pets:
def __init__(self):
self.conf = Config.get_conf(self, 1234567890)
# Here we'll assign some default costs for the pets
self.conf.register_global(
dog=100,
cat=100,
bird=50
)
self.conf.register_user(
pets={}
)
And now that the cog is set up we'll need to create some commands that allow users to purchase these pets::
# continued
@commands.command()
async def get_pet(self, ctx, pet_type: str, pet_name: str):
"""
Purchase a pet.
Pet type must be one of: dog, cat, bird
"""
# Now we need to determine what the cost of the pet is and
# if the user has enough credits to purchase it.
# We will need to use "get_raw"
try:
cost = await self.conf.get_raw(pet_type)
except KeyError:
# KeyError is thrown whenever the data you try to access does not
# exist in the registered defaults or in the saved data.
await ctx.send("Bad pet type, try again.")
return
After we've determined the cost of the pet we need to check if the user has enough credits and then we'll need to
assign a new pet to the user. This is very easily done using the V3 bank API and :py:meth:`Group.set_raw`::
# continued
if await bank.can_spend(ctx.author, cost):
await self.conf.user(ctx.author).pets.set_raw(
pet_name, value={'cost': cost, 'hunger': 0}
)
# this is equivalent to doing the following
pets = await self.conf.user(ctx.author).pets()
pets[pet_name] = {'cost': cost, 'hunger': 0}
await self.conf.user(ctx.author).pets.set(pets)
Since the pets can get hungry we're gonna need a command that let's pet owners check how hungry their pets are::
# continued
@commands.command()
async def hunger(self, ctx, pet_name: str):
try:
hunger = await self.conf.user(ctx.author).pets.get_raw(pet_name, 'hunger')
except KeyError:
# Remember, this is thrown if something in the provided identifiers
# is not found in the saved data or the defaults.
await ctx.send("You don't own that pet!")
return
await ctx.send("Your pet has {}/100 hunger".format(hunger))
We're responsible pet owners here, so we've also got to have a way to feed our pets::
# continued
@commands.command()
async def feed(self, ctx, pet_name: str, food: int):
# This is a bit more complicated because we need to check if the pet is
# owned first.
try:
pet = await self.conf.user(ctx.author).pets.get_raw(pet_name)
except KeyError:
# If the given pet name doesn't exist in our data
await ctx.send("You don't own that pet!")
return
hunger = pet.get("hunger")
# Determine the new hunger and make sure it doesn't go negative
new_hunger = max(hunger - food, 0)
await self.conf.user(ctx.author).pets.set_raw(
pet_name, 'hunger', value=new_hunger
)
# We could accomplish the same thing a slightly different way
await self.conf.user(ctx.author).pets.get_attr(pet_name).hunger.set(new_hunger)
await ctx.send("Your pet is now at {}/100 hunger!".format(new_hunger)
Of course, if we're less than responsible pet owners, there are consequences::
#continued
@commands.command()
async def adopt(self, ctx, pet_name: str, *, member: discord.Member):
try:
pet = await self.conf.user(member).pets.get_raw(pet_name)
except KeyError:
await ctx.send("That person doesn't own that pet!")
return
hunger = pet.get("hunger")
if hunger < 80:
await ctx.send("That pet is too well taken care of to be adopted.")
return
await self.conf.user(member).pets.clear_raw(pet_name)
# this is equivalent to doing the following
pets = await self.conf.user(member).pets()
del pets[pet_name]
await self.conf.user(member).pets.set(pets)
await self.conf.user(ctx.author).pets.set_raw(pet_name, value=pet)
await ctx.send(
"Your request to adopt this pet has been granted due to "
"how poorly it was taken care of."
)
*************
V2 Data Usage
*************
There has been much conversation on how to bring V2 data into V3 and, officially, we recommend that cog developers
make use of the public interface in Config (using the categories as described in these docs) rather than simply
copying and pasting your V2 data into V3. Using Config as recommended will result in a much better experience for
you in the long run and will simplify cog creation and maintenance.
However.
We realize that many of our cog creators have expressed disinterest in writing converters for V2 to V3 style data.
As a result we have opened up config to take standard V2 data and allow cog developers to manipulate it in V3 in
much the same way they would in V2. The following examples will demonstrate how to accomplish this.
.. warning::
By following this method to use V2 data in V3 you may be at risk of data corruption if your cog is used on a bot
with multiple shards. USE AT YOUR OWN RISK.
.. code-block:: python
from redbot.core import Config
class ExampleCog:
def __init__(self):
self.conf = Config.get_conf(self, 1234567890)
self.data = {}
async def load_data(self):
self.data = await self.conf.custom("V2", "V2").all()
async def save_data(self):
await self.conf.custom("V2", "V2").set(self.data)
async def setup(bot):
cog = ExampleCog()
await cog.load_data()
bot.add_cog(cog)
*************
API Reference
*************
@@ -185,6 +374,21 @@ API Reference
inside the bot itself! Simply take a peek inside of the :code:`tests/core/test_config.py` file for examples of using
Config in all kinds of ways.
.. important::
When getting, setting or clearing values in Config, all keys are casted to `str` for you. This
includes keys within a `dict` when one is being set, as well as keys in nested dictionaries
within that `dict`. For example::
>>> conf = Config.get_conf(self, identifier=999)
>>> conf.register_global(foo={})
>>> await conf.foo.set_raw(123, value=True)
>>> await conf.foo()
{'123': True}
>>> await conf.foo.set({123: True, 456: {789: False}}
>>> await conf.foo()
{'123': True, '456': {'789': False}}
.. automodule:: redbot.core.config
Config

View File

@@ -1,10 +0,0 @@
.. red invocation context documentation
==========================
Command Invocation Context
==========================
.. automodule:: redbot.core.context
.. autoclass:: redbot.core.RedContext
:members:

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

@@ -13,11 +13,12 @@ Basic Usage
.. code-block:: python
from discord.ext import commands
from redbot.core.i18n import CogI18n
from redbot.core import commands
from redbot.core.i18n import Translator, cog_i18n
_ = CogI18n("ExampleCog", __file__)
_ = Translator("ExampleCog", __file__)
@cog_i18n(_)
class ExampleCog:
"""description"""
@@ -39,16 +40,19 @@ In a command prompt in your cog's package (where yourcog.py is),
create a directory called "locales".
Then do one of the following:
Windows: :code:`python <your python install path>\Tools\i18n\pygettext.py -n -p locales`
Windows: :code:`python <your python install path>\Tools\i18n\pygettext.py -D -n -p locales`
Mac: ?
Linux: :code:`pygettext3 -n -p locales`
Linux: :code:`pygettext3 -D -n -p locales`
This will generate a messages.pot file with strings to be translated
This will generate a messages.pot file with strings to be translated, including
docstrings.
-------------
API Reference
-------------
.. automodule:: redbot.core.i18n
.. automodule:: redbot.core.i18n
:members:
:special-members: __call__

63
docs/framework_rpc.rst Normal file
View File

@@ -0,0 +1,63 @@
.. rpc docs
===
RPC
===
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

@@ -4,14 +4,56 @@
Utility Functions
=================
General Utility
===============
.. automodule:: redbot.core.utils
:members: deduplicate_iterables, bounded_gather, bounded_gather_iter
Chat Formatting
===============
.. automodule:: redbot.core.utils.chat_formatting
:members:
Embed Helpers
=============
.. automodule:: redbot.core.utils.embed
:members:
Reaction Menus
==============
.. automodule:: redbot.core.utils.menus
:members:
Event Predicates
================
.. automodule:: redbot.core.utils.predicates
:members:
Mod Helpers
===========
.. automodule:: redbot.core.utils.mod
:members:
V2 Data Conversion
==================
.. automodule:: redbot.core.utils.data_converter
:members: DataConverter
Tunnel
======
.. automodule:: redbot.core.utils.tunnel
:members: Tunnel
Common Filters
==============
.. automodule:: redbot.core.utils.common_filters
:members:

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
:code:`pip install --process-dependency-links -U git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=redbot[test]`
To start off, be sure that you have installed Python 3.6.2 or higher (3.6.6 or higher on Windows).
Open a terminal or command prompt and type :code:`pip install -U git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=redbot[test]`
(note that if you get an error with this, try again but put :code:`python -m` in front of the command
This will install the latest version of V3.
This will install the latest version of V3.
--------------------
Setting up a package
@@ -45,7 +44,7 @@ In that file, place the following code:
.. code-block:: python
from discord.ext import commands
from redbot.core import commands
class Mycog:
"""My custom cog"""
@@ -90,6 +89,6 @@ have successfully created a cog!
Additional resources
--------------------
Be sure to check out the `migration guide </guide_migration>`_ for some resources
Be sure to check out the :doc:`/guide_migration` for some resources
on developing cogs for V3. This will also cover differences between V2 and V3 for
those who developed cogs for V2.

View File

@@ -0,0 +1,154 @@
.. Converting Data from a V2 cog
.. role:: python(code)
:language: python
============================
Importing Data From a V2 Cog
============================
This guide serves as a tutorial on using the DataConverter class
to import settings from a V2 cog.
------------------
Things you'll need
------------------
1. The path where each file holding related settings in v2 is
2. A conversion function to take the data and transform it to conform to Config
-----------------------
Getting your file paths
-----------------------
You should probably not try to find the files manually.
Asking the user for the base install path and using a relative path to where the
data should be, then testing that the file exists there is safer. This is especially
True if your cog has multiple settings files
Example
.. code-block:: python
from discord.ext import commands
from pathlib import Path
@commands.command(name="filefinder")
async def file_finding_command(self, ctx, filepath):
"""
this finds a file based on a user provided input and a known relative path
"""
base_path = Path(filepath)
fp = base_path / 'data' / 'mycog' / 'settings.json'
if not fp.is_file():
pass
# fail, prompting user
else:
pass
# do something with the file
---------------
Converting data
---------------
Once you've gotten your v2 settings file, you'll want to be able to import it
There are a couple options available depending on how you would like to convert
the data.
The first one takes a data path, and a conversion function and does the rest for you.
This is great for simple data that just needs to quickly be imported without much
modification.
Here's an example of that in use:
.. code-block:: python
from pathlib import Path
from discord.ext import commands
from redbot.core.utils.data_converter import DataConverter as dc
from redbot.core.config import Config
...
async def import_v2(self, file_path: Path):
"""
to be called from a command limited to owner
This should be a coroutine as the convert function will
need to be awaited
"""
# First we give the converter our cog's Config instance.
converter = dc(self.config)
# next we design a way to get all of the data into Config's internal
# format. This should be a generator, but you can also return a single
# list with identical results outside of memory usage
def conversion_spec(v2data):
for guild_id in v2.data.keys():
yield {(Config.GUILD, guild_id): {('blacklisted',): True}}
# This is yielding a dictionary that is designed for config's set_raw.
# The keys should be a tuple of Config scopes + the needed Identifiers. The
# values should be another dictionary whose keys are tuples representing
# config settings, the value should be the value to set for that.
# Then we pass the file and the conversion function
await converter.convert(file_path, conversion_spec)
# From here, our data should be imported
You can also choose to convert all of your data and pass it as a single dict
This can be useful if you want finer control over the dataconversion or want to
preserve any data from v3 that may share the same entry and set it aside to prompt
a user
.. code-block:: python
from pathlib import Path
from discord.ext import commands
from redbot.core.utils.data_converter import DataConverter as dc
from redbot.core.config import Config
...
await dc(config_instance).dict_import(some_processed_dict)
The format of the items of the dict is the same as in the above example
-----------------------------------
Config Scopes and their Identifiers
-----------------------------------
This section is provided as a quick reference for the identifiers for default
scopes available in Config. This does not cover usage of custom scopes, though the
data converter is compatible with those as well.
Global::
:code:`(Config.GLOBAL,)`
Guild::
:code:`(Config.GUILD, guild_id)`
Channel::
:code:`(Config.CHANNEL, channel_id)`
User::
:code:`(Config.USER, user_id)`
Member::
:code:`(Config.MEMBER, guild_id, user_id)`
Role::
:code:`(Config.ROLE, role_id)`
-----------------------------
More information and Examples
-----------------------------
For a more in depth look at how all of these commands function
You may want to take a look at how core data is being imported
:code:`redbot/cogs/dataconverter/core_specs.py`

View File

@@ -11,17 +11,18 @@ Welcome to Red - Discord Bot's documentation!
:caption: Installation Guides:
install_windows
install_mac
install_ubuntu
install_debian
install_centos
install_raspbian
install_linux_mac
venv_guide
cog_dataconverter
autostart_systemd
.. toctree::
:maxdepth: 2
:caption: Cog Reference:
cog_customcom
cog_downloader
cog_permissions
.. toctree::
:maxdepth: 2
@@ -29,14 +30,19 @@ Welcome to Red - Discord Bot's documentation!
guide_migration
guide_cog_creation
guide_data_conversion
framework_bank
framework_bot
framework_checks
framework_cogmanager
framework_datamanager
framework_commands
framework_config
framework_datamanager
framework_downloader
framework_events
framework_i18n
framework_modlog
framework_context
framework_rpc
framework_utils

View File

@@ -1,54 +0,0 @@
.. centos install guide
==========================
Installing Red on CentOS 7
==========================
---------------------------
Installing pre-requirements
---------------------------
.. code-block:: none
yum -y groupinstall development
yum -y install https://centos7.iuscommunity.org/ius-release.rpm
yum -y install yum-utils wget which python35u python35u-pip python35u-devel openssl-devel libffi-devel git opus-devel
sh -c "$(wget https://gist.githubusercontent.com/mustafaturan/7053900/raw/27f4c8bad3ee2bb0027a1a52dc8501bf1e53b270/latest-ffmpeg-centos6.sh -O -)"
--------------
Installing Red
--------------
Without audio:
:code:`pip3 install -U --process-dependency-links red-discordbot`
With audio:
:code:`pip3 install -U --process-dependency-links red-discordbot[voice]`
To install the development version (without audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot`
To install the development version (with audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice]`
----------------------
Setting up an instance
----------------------
Run :code:`redbot-setup` and follow the prompts. It will ask first for where you want to
store the data (the default is :code:`~/.local/share/Red-DiscordBot`) and will then ask
for confirmation of that selection. Next, it will ask you to choose your storage backend
(the default here is JSON). It will then ask for a name for your instance. This can be
anything as long as it does not contain spaces; however, keep in mind that this is the
name you will use to run your bot, and so it should be something you can remember.
-----------
Running Red
-----------
Run :code:`redbot <your instance name>` and run through the initial setup. This will ask for
your token and a prefix.

View File

@@ -1,55 +0,0 @@
.. debian install guide
================================
Installing Red on Debian Stretch
================================
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, make a new one.
---------------------------
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 ffmpeg libopus-dev unzip -y
------------------
Installing the bot
------------------
To install without audio:
:code:`pip3 install -U --process-dependency-links red-discordbot`
To install with audio:
:code:`pip3 install -U --process-dependency-links red-discordbot[voice]`
To install the development version (without audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot`
To install the development version (with audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice]`
------------------------
Setting up your instance
------------------------
Run :code:`redbot-setup` and follow the prompts. It will ask first for where you want to
store the data (the default is :code:`~/.local/share/Red-DiscordBot`) and will then ask
for confirmation of that selection. Next, it will ask you to choose your storage backend
(the default here is JSON). It will then ask for a name for your instance. This can be
anything as long as it does not contain spaces; however, keep in mind that this is the
name you will use to run your bot, and so it should be something you can remember.
-----------
Running Red
-----------
Run :code:`redbot <your instance name>` and run through the initial setup. This will ask for
your token and a prefix.

203
docs/install_linux_mac.rst Normal file
View File

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

View File

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

View File

@@ -1,53 +0,0 @@
.. raspbian install guide
==================================
Installing Red on Raspbian Stretch
==================================
---------------------------
Installing pre-requirements
---------------------------
.. code-block:: none
sudo apt-get install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git libav-tools libopus-dev unzip -y
--------------
Installing Red
--------------
Without audio:
:code:`pip3 install -U --process-dependency-links red-discordbot`
With audio:
:code:`pip3 install -U --process-dependency-links red-discordbot[voice]`
To install the development version (without audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot`
To install the development version (with audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice]`
----------------------
Setting up an instance
----------------------
Run :code:`redbot-setup` and follow the prompts. It will ask first for where you want to
store the data (the default is :code:`~/.local/share/Red-DiscordBot`) and will then ask
for confirmation of that selection. Next, it will ask you to choose your storage backend
(the default here is JSON). It will then ask for a name for your instance. This can be
anything as long as it does not contain spaces; however, keep in mind that this is the
name you will use to run your bot, and so it should be something you can remember.
-----------
Running Red
-----------
Run :code:`redbot <your instance name>` and run through the initial setup. This will ask for
your token and a prefix.
.. warning:: Audio will not work on Raspberry Pi's **below** 2B. This is a CPU problem and *cannot* be fixed.

View File

@@ -1,53 +0,0 @@
.. ubuntu 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.
-------------------------------
Installing the pre-requirements
-------------------------------
.. code-block:: none
sudo apt install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git ffmpeg libopus-dev unzip -y
------------------
Installing the bot
------------------
To install without audio:
:code:`pip3 install -U --process-dependency-links red-discordbot`
To install with audio:
:code:`pip3 install -U --process-dependency-links red-discordbot[voice]`
To install the development version (without audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot`
To install the development version (with audio):
:code:`pip3 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice]`
------------------------
Setting up your instance
------------------------
Run :code:`redbot-setup` and follow the prompts. It will ask first for where you want to
store the data (the default is :code:`~/.local/share/Red-DiscordBot`) and will then ask
for confirmation of that selection. Next, it will ask you to choose your storage backend
(the default here is JSON). It will then ask for a name for your instance. This can be
anything as long as it does not contain spaces; however, keep in mind that this is the
name you will use to run your bot, and so it should be something you can remember.
-----------
Running Red
-----------
Run :code:`redbot <your instance name>` and run through the initial setup. This will ask for
your token and a prefix.

View File

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

View File

@@ -1,4 +0,0 @@
sphinx==1.6.3
sphinxcontrib-asyncio
sphinx_rtd_theme
git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py[voice]

132
docs/venv_guide.rst Normal file
View File

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

View File

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

43
make.bat Normal file
View File

@@ -0,0 +1,43 @@
@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 -N !PYFILES!
exit /B %ERRORLEVEL%
:stylecheck
black -l 99 -N --check !PYFILES!
exit /B %ERRORLEVEL%
:update_vendor
if [%REF%] == [] (
set REF2="rewrite"
) else (
set REF2=%REF%
)
pip install --upgrade --no-deps -t . https://github.com/Rapptz/discord.py/archive/!REF2!.tar.gz#egg=discord.py
del /S /Q "discord.py*-info"
for /F %%i in ('dir /S /B discord.py*.egg-info') do rmdir /S /Q %%i
goto reformat
: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.
echo update_vendor Update vendored discord.py library to %%REF%%, which defaults to
echo "rewrite"

View File

@@ -1,11 +1,34 @@
import sys
import typing
import warnings
import discord
import colorama
# Let's do all the dumb version checking in one place.
if sys.platform == "win32":
# Due to issues with ProactorEventLoop prior to 3.6.6 (bpo-26819)
MIN_PYTHON_VERSION = (3, 6, 6)
else:
MIN_PYTHON_VERSION = (3, 6, 2)
if sys.version_info < MIN_PYTHON_VERSION:
print(
f"Python {'.'.join(map(str, MIN_PYTHON_VERSION))} is required to run Red, but you have "
f"{sys.version}! Please update Python."
)
sys.exit(1)
if discord.version_info.major < 1:
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 or greater."
)
sys.exit(1)
colorama.init()
# Filter fuzzywuzzy slow sequence matcher warning
warnings.filterwarnings("ignore", module=r"fuzzywuzzy.*")
# Prevent discord PyNaCl missing warning
discord.voice_client.VoiceClient.warn_nacl = False

View File

@@ -6,19 +6,31 @@ 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
import os
# Let's not force this dependency, uvloop is much faster on cpython
if sys.implementation.name == "cpython":
try:
import uvloop
except ImportError:
pass
else:
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
if sys.platform == "win32":
asyncio.set_event_loop(asyncio.ProactorEventLoop())
#
# Red - Discord Bot v3
@@ -40,24 +52,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)
logger.setLevel(logging.INFO)
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 +89,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()
@@ -96,16 +111,27 @@ def list_instances():
def main():
description = "Red V3"
cli_flags = parse_cli_flags(sys.argv[1:])
if cli_flags.list_instances:
list_instances()
elif not cli_flags.instance_name:
elif cli_flags.version:
print(description)
sys.exit(0)
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)
description = "Red - Version {}".format(__version__)
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))
@@ -115,30 +141,30 @@ 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))
loop.run_until_complete(red.start(token, bot=True))
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.")
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("> "):
@@ -153,15 +179,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:
loop.run_until_complete(red.rpc.close())
except AttributeError:
pass
sys.exit(red._shutdown_mode.value)
if __name__ == '__main__':
if __name__ == "__main__":
main()

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