Compare commits

...

757 Commits

Author SHA1 Message Date
Chiggy-Playz
96153bb177
Fix slash command scope image in docs (#104)
This time its visible properly
2021-10-30 17:18:11 +01:00
Gnome
babbb22462
Fix small typing issue 2021-10-29 14:06:44 +01:00
Gnome
eef8c07379
Optimise _unwrap_slash_groups and similar 2021-10-29 13:33:15 +01:00
Chiggy-Playz
be9e693047
Fix Literal inside Optional not showing choices (#98) 2021-10-27 14:00:21 +01:00
Chiggy-Playz
351bc5bc19
Add Protocol Urls (#103)
Co-authored-by: Stocker <44980366+StockerMC@users.noreply.github.com>
Co-authored-by: Gnome! <45660393+Gnome-py@users.noreply.github.com>
2021-10-27 13:32:50 +01:00
Gnome
5bb88062fa
Basic interaction autocomplete support 2021-10-26 12:27:31 +01:00
Gnome
e99ee71233
Add ctx.defer to help with 3 second slash command response rule.
Acts as `ctx.interaction.response.defer` or loops `ctx.trigger_typing` depending on context.
2021-10-23 21:19:51 +01:00
iDutchy
63dbecf65d
Fix incorrect doc
I forgot about the decorator... min_values can also be 0, so this should prevent confusion
2021-10-20 02:04:28 +02:00
iDutchy
f46d3bfa28
fix incorrect doc
As it seems, this stated min_values must be between 1-25 even tho docs state it must be between 0-25. This changes that doc so that it might prevent confusion in the future
2021-10-20 01:54:11 +02:00
Stocker
983cbb3161
Add the ability to set the option name with commands.Option (#102)
* Add the ability to set the option name with commands.Option
* Document commands.Option.name
2021-10-16 15:00:56 +01:00
Soheab
838d9d8986
Add ability to set a flag description. (#99)
* Add ability to set a flag description.

This PR adds the ability to set a flag description that shows in the slash command options menu.
2021-10-16 13:27:02 +01:00
Chiggy-Playz
e0bf2f9121
Add Channel types support (#100) 2021-10-13 17:34:13 +01:00
Gnome
0abac8698d
Fix slash command flag parsing
Also removes the extra space at the end of fake message content
2021-10-08 20:06:05 +01:00
Gnome
d781af8be5
Remove maintainer list from README.rst
This list became outdated straight away, and is a bad idea in general.
2021-10-08 18:24:22 +01:00
Gnome
9e31aad96d Fix code style issues with Black 2021-10-07 17:34:29 +01:00
Chiggy-Playz
eca1d9a470
Sort events by categories (#88) 2021-10-07 16:48:38 +01:00
Duck
0bbcfd7f33
Update resource links (#65)
* Updated links

* Remove github discussions from getting help
2021-10-06 20:32:48 +01:00
Ian Webster
ec1e2add21
Update user-agent (#92) 2021-10-04 21:11:10 +01:00
Gnome
4277f65051 Implement _FakeSlashMessage.clean_content
Closes #83
2021-10-03 21:05:00 +01:00
Gnome!
3260ec6643
Add improved docs for slash commands (#77)
* Fix command checks actually working

* Current progress on slash command docs

* Improve docs for slash commands further
2021-09-27 01:14:07 -07:00
Chiggy-Playz
d16d2d856f
Sort subcommand names (#68) 2021-09-25 22:43:23 -07:00
Gnome!
456d71d228
Add better support for MENTIONABLE (#74) 2021-09-25 22:41:43 -07:00
Gnome!
093a38527d
Fix slash command prefix to / (#75) 2021-09-25 22:40:35 -07:00
NORXND
163d8e6586
Merge pull request #76
* Fix docs in BadInviteArgument class
2021-09-25 22:39:09 -07:00
Tom
0637a628ca
update workflows (#73)
* modify workflows to fit into one file, fix pyright workflow

* remove redundant pip install

* add check flag to black

* use psf/black for black checker
2021-09-21 14:51:46 -07:00
Gnome!
02c6b07834
Merge pull request #72
* Fix command checks actually working
2021-09-21 14:34:54 -07:00
Gnome!
b810848273
Merge pull request #70
* Fix embed image/thumbnail property
2021-09-21 12:10:16 -07:00
Astrea
cd4bb296f3
Merge pull request #58
* FIxed `userinfo` command not returning an avatar...

* Quick merge conflict fix

* Merge branch '2.0' into converter-example-fix

* Fix code style issues with Black
2021-09-21 11:52:55 -07:00
Arnav Jindal
6a63ce2ed7
Add typechecking for PRS/Commits (#59)
* Create ci.yml

* Create .python-black

* Remove linting
2021-09-21 11:52:03 -07:00
Gnome!
fba7ca420c
Merge pull request #63
* Add ephemeral attachment field

* I did not miss a comma
2021-09-21 11:51:23 -07:00
Gnome!
e65415d3c8
Merge pull request #60
* Rework how checks add attributes to Commmand

* Merge remote-tracking branch 'upstream/2.0' into command-attrs-checks
2021-09-21 11:47:28 -07:00
Astrea
2ecf755372
Merge pull request #57
* FIx _accent_colour being improperly typehinted
2021-09-21 11:37:28 -07:00
Gnome!
00ae8bb18c
Fix all invites to devision server invite (#69) 2021-09-20 21:25:48 +02:00
iDutchy
0638bda719
Fix docs invite
Invite link on docs was still set to dpy, this changes it to edpy
2021-09-19 02:42:43 +02:00
Gnome!
1957fa6011
Implement a least breaking approach to slash commands (#39)
* Most slash command support completed, needs some debugging (and reindent)

* Implement a ctx.send helper for slash commands

* Add group command support

* Add Option converter, fix default optional, fix help command

* Add client.setup and move readying commands to that

* Implement _FakeSlashMessage.from_interaction

* Rename normmal_command to message_command

* Add docs for added params

* Add slash_command_guilds to bot and decos

* Fix merge conflict

* Remove name from commands.Option, wasn't used

* Move slash command processing to BotBase.process_slash_commands

* Create slash_only.py

Basic example for slash commands

* Create slash_and_message.py

Basic example for mixed commands

* Fix slash_command and normal_command bools

* Add some basic error handling for registration

* Fixed converter upload errors

* Fix some logic and make an actual example

* Thanks Safety Jim

* docstrings, *args, and error changes

* Add proper literal support

* Add basic documentation on slash commands

* Fix non-slash command interactions

* Fix ctx.reply in slash command context

* Fix typing on Context.reply

* Fix multiple optional argument sorting

* Update ctx.message docs to mention error instead of warning

* Move slash command creation to BotBase

* Fix code style issues with Black

* Rearrange some stuff and add flag support

* Change some errors and fix interaction.channel fixing

* Fix slash command quoting for *args

Co-authored-by: iDutchy <42503862+iDutchy@users.noreply.github.com>
Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
2021-09-19 01:28:11 +02:00
Astrea
75a23351c4
Revert #42 (#61) 2021-09-09 00:02:02 +02:00
Lint Action
7513c2138f Fix code style issues with Black 2021-09-05 21:34:20 +00:00
IAmTomahawkx
a23dae8604 Merge branch '2.0' of https://github.com/IDevision/enhanced-discord.py into 2.0 2021-09-05 14:33:00 -07:00
IAmTomahawkx
1833e984ce add black workflow, change our code formats. closes #43 2021-09-05 14:32:51 -07:00
Gnome!
53a6b2cb45
Revert "Merge pull request #12" (#56)
This reverts commit 42c0a8d8a5840c00185e367933e61e2565bf7305.
2021-09-05 10:37:51 -07:00
iDutchy
65640ddfc7
Merge pull request #55 from TheMoksej/patch-2
remove unnecessary await
2021-09-05 15:24:45 +02:00
Moksej
14b3188bb8
remove unnecessary await 2021-09-05 13:58:10 +02:00
Arthur
3ffe134895
Merge pull request #44
* Typehint gateway.py

* Add relevant typehints to gateway.py to voice_client.py

* Change EventListener to subclass NamedTuple

* Add return type for DiscordWebSocket.wait_for

* Correct deque typehint

* Remove unnecessary typehints for literals

* Use type aliases

* Merge branch '2.0' into pr7422
2021-09-02 13:50:19 -07:00
Arthur
1032728311
Merge pull request #32
* Add get/fetch_member to ThreadMember objects
2021-09-02 13:43:19 -07:00
Arthur
33470ff196
Merge pull request #31
* Add bots and humans to TextChannel
2021-09-02 13:41:26 -07:00
Arthur
47e42d1648
Merge pull request #42
* implement WelcomeScreen

* copy over the kwargs issue.

* readable variable names

* modernise code

* modernise pt2

* Update discord/welcome_screen.py

* make pylance not cry from my onions

* type http.py

* remove extraneous import
2021-09-02 13:40:11 -07:00
Astrea
4055bafaa5
Merge pull request #47
* Added `on_raw_typing` event
2021-09-02 13:34:39 -07:00
IAmTomahawkx
152b61aabb fix recursionerror caused by a Pull Request 2021-09-02 12:49:38 -07:00
Ahmad Ansori Palembani
f37be7961a
Merge pull request #41
* Fixed `TypeError`

* Handles `EmptyEmbed` inside setter instead of set_

* Remove return and setter docstring
2021-09-02 12:46:56 -07:00
NightSlasher35
0f6db99c59
Merge pull request #22
* add nitro booster color

* Update discord/colour.py
2021-09-02 12:34:41 -07:00
chillymosh
42c0a8d8a5
Merge pull request #12
* Clean up python

* Clean up bot python

* revert lists

* revert commands.bot completely

* extract raise_expected_coro further

* add new lines

* removed erroneous import

* remove hashed line
2021-09-02 12:32:46 -07:00
Arthur
092fbca08f
Merge pull request #21
* [BREAKING] Make case_insensitive default to True on groups and commands
2021-09-02 12:28:03 -07:00
Arthur
13834d1147
Merge pull request #7
* Add try_user to get a user from cache or from the gateway.

* Extract populate_owners into a new coroutine.

* Add a try_owners coroutine to get a list of owners of the bot.

* Fix coding-style.

* Fix a bug where None would be returned in try_owners if the cache was…

* Fix docstring

* Add spacing in the code
2021-09-02 12:24:52 -07:00
Arthur
5d10384576
Merge pull request #27
* Add author_permissions to the Context object as a shortcut to return …
2021-09-02 12:18:26 -07:00
Daud
fc0188d7bc
Merge pull request #49
* Change README title to enhanced-discord.py
2021-09-02 12:17:19 -07:00
iDutchy
a62c0ff0d1
Merge pull request #16 from paris-ci/alias_administrator_to_admin
Alias admin to administrators in permissions. This needs to be tested…
2021-09-02 03:06:30 +02:00
iDutchy
b75be64044
Update permissions.py
A better implementation :)
2021-09-02 03:05:49 +02:00
NightSlasher35
630a842556
Update CONTRIBUTING.md correctly (#29)
* Update CONTRIBUTING.md

* Update CONTRIBUTING.md

Co-authored-by: Tom <47765953+IAmTomahawkx@users.noreply.github.com>
2021-09-01 17:49:41 -07:00
Arthur
c485e08ea0
Add try_member to guild. (#14)
* Add try_member to guild.

This also fix an omission in the fetch_member docs. fetch_member raises NotFound if the given user isn't in the guild.

* Optimize imports.
2021-09-01 17:47:15 -07:00
classerase
dba9a8abb9
Update README.rst (#48) 2021-09-01 15:30:15 -07:00
iDutchy
6f5614373a
Merge pull request #15 from WhoTheOOF/patch-3
Add original dark blurple
2021-09-01 04:40:12 +02:00
iDutchy
2e12746c70
Merge pull request #11 from TheMoksej/patch-2
versionadded needs to be added here
2021-09-01 04:38:26 +02:00
iDutchy
5ef72e4f70
Merge pull request #20 from paris-ci/special_methods
Special methods
2021-09-01 04:27:33 +02:00
iDutchy
7e18d30820
Merge pull request #17 from Astrea49/2.0
Prefer `static_format` over `format` with static assets
2021-09-01 04:26:31 +02:00
iDutchy
923a6a885d
Merge pull request #13 from paris-ci/rework_set_in_embeds
Make `Embed.image` and `Embed.thumbnail` full-featured properties
2021-09-01 04:24:57 +02:00
iDutchy
b28893aa36
Merge pull request #40 from Gnome-py/required-intents
Remove intents.default and make intents a required parameter
2021-09-01 04:21:37 +02:00
Gnome
6e41bd2219 Remove intents.default and make intents a required parameter 2021-08-31 20:53:54 +01:00
Moksej
773ad6f5bf
add back the silent kwarg to message.delete (#9)
* add back the silent kwarg to message.delete

* forgot about versionadded

* shorten the if statement

* simplify raising a bit ig?

* should be versionchanged instead

Co-authored-by: Arthur <site-github@api-d.com>

* remove `Optional` from parameter and doc string

Co-authored-by: Arthur <site-github@api-d.com>
2021-08-29 10:57:07 -07:00
Arthur
de0e8ef108
V2.0 changelog (#8)
* Copy in messages from Danny, verbatim

* Type whats_new

* Add my changes to the changelog

* Fix a typo
2021-08-29 10:55:49 -07:00
Arthur Jovart
64ee792391
Add int() support to Hashable, making it available across the board for AuditLogEntry, *Channel, Guild, Object, Message, ... 2021-08-29 01:21:20 +02:00
Arthur Jovart
22de755059
Add int() and str() support to Message 2021-08-29 01:09:05 +02:00
Arthur Jovart
fa7f8efc8e
Add int() support to Guild 2021-08-29 01:07:26 +02:00
Arthur Jovart
9d1df65af3
Add int() support to Role 2021-08-29 01:06:18 +02:00
Arthur Jovart
3ce86f6cde
Add int() support to Emoji 2021-08-29 01:05:28 +02:00
Arthur Jovart
31e3e99c2b
Add __int__ special method to User and Member 2021-08-29 00:59:29 +02:00
Jadon
cc90d312f5
Add original dark blurple
This adds the old discord dark blurple color as a classmethod for embeds and whatever.
2021-08-28 17:26:46 -05:00
Arthur Jovart
8cdc1f4ad9
Alias admin to administrators in permissions. This needs to be tested, but should be working. 2021-08-29 00:26:26 +02:00
Sonic4999
75f052b8c9 Prefer static_format over format with static assets 2021-08-28 18:24:05 -04:00
Arthur Jovart
c8cdb275c5
Fix set_* function name 2021-08-28 23:29:49 +02:00
Arthur Jovart
406f0ffe04
Make Embed.image and Embed.thumbnail full-featured properties
This avoids the need for set_* methods.
2021-08-28 23:14:26 +02:00
Moksej
a4acbd2e08
versionadded needs to be added here 2021-08-28 22:10:58 +02:00
Tom
6bcc717e63
Merge pull request #6 from paris-ci/guild-bots-humans
Add humans and bots to Guild.
2021-08-28 12:16:45 -07:00
Arthur Jovart
86618f42a6
Add humans and bots to Guild. 2021-08-28 20:34:57 +02:00
Tom
9d474b92f6
Merge pull request #5 from paris-ci/readme-fixes
Update the README for the fork version.
2021-08-28 11:25:20 -07:00
IAmTomahawkx
96ac8c1a4f remove JA docs / README.ja.rst 2021-08-28 11:23:10 -07:00
Arthur Jovart
cf4d2e23a2
Update the README for the fork version. Also removes the japanese version. 2021-08-28 19:44:58 +02:00
Rapptz
45d498c1b7 Fix README link 2021-08-27 20:59:11 -04:00
Rapptz
7b87d2dec0 Add gist to README 2021-08-27 20:55:36 -04:00
James Gayfer
41d22f4312
Fix all_channel permissions
When the new permission for send_messages_in_threads was added, we added
the wrong bit. Instead of adding the bit as the most significant (37),
we added it as the last significant, which invalidated most of the
permissions defined by this method.
2021-08-27 20:04:18 -04:00
Imayhaveborkedit
12dcc7c44b
Rearrange player cleanup code
Since apparently closing stdin and later calling communicate() is no bueno, 
we're just going to rearrange the process finalization code so both cleanup()
and the pipe loop exit conditions point to it.
2021-08-27 19:40:31 -04:00
jack1142
b12fe223b2
Fix description of Data Classes section in api.rst 2021-08-27 18:13:55 -04:00
Nadir Chowdhury
a2a7b0f076
[tasks] Improve typing parity 2021-08-27 17:18:15 -04:00
ShashankKumarSaxena
b2ac327bd8
[commands] Fix a small typo in Context.invoke docs 2021-08-27 16:02:10 -04:00
Abhinav Singh
f485f1b612
Fix a typo in docstrings of Client class 2021-08-27 16:01:44 -04:00
Rapptz
29b808d33f Change avatar_url type hint to Any instead of str 2021-08-27 05:41:00 -04:00
numbermaniac
516675dd2e
Fix a typo in member_role_update 2021-08-27 02:05:44 -04:00
Chrovo
932efa1edc
Change a few parameters to be positional only
This makes parameters positional only in the methods 
fetch_sticker, fetch_webhook, fetch_channel, fetch_user, fetch_widget, 
fetch_stage_instance, fetch_guild, get_sticker, and get_channel.
2021-08-26 20:47:24 -04:00
Steve C
059ec161f8
Fix Webhook return types
Also add positional only arguments where applicable
2021-08-26 20:46:25 -04:00
Danny
e7821be4aa
Add default value for invitable in HTTPClient 2021-08-26 17:51:49 -04:00
James Gayfer
4aafa39e8c
Update thread permissions
Discord has renamed / repurposed "Use Public Threads" and "Use Private
Threads", as well as added a new permission "Send Messages in Threads".

For more information, see:
  https://github.com/discord/discord-api-docs/pull/3672
2021-08-26 15:53:21 -04:00
Josh
d2ea33e5e9
Add support for invitable thread option 2021-08-26 15:52:07 -04:00
pyxiis
2f2c39ed22
Add Client.status attribute 2021-08-26 15:50:21 -04:00
Izhar Ahmad
efec816de2
Make arguments positional only in 2 get methods
- Member.get_role
- Guild.get_member
2021-08-25 20:52:20 -04:00
Rapptz
dd7d4b8e7f Use a thread values view when constructing TextChannel.threads
Avoids an accidental O(n^2) situation.
2021-08-25 20:42:21 -04:00
Rapptz
2d8f299b6b Use MISSING instead of None for HelpCommand.context
It's basically just late-init
2021-08-25 09:53:55 -04:00
Rapptz
3382d2e9e8 Update documentation on select limits 2021-08-25 09:21:49 -04:00
Rapptz
539577a2dd Bring back ParamSpec in utils 2021-08-25 05:30:41 -04:00
Rapptz
4f8e67998a Fix copy_doc typing to not error due to overloaded ParamSpec 2021-08-25 03:41:24 -04:00
Rapptz
848d752388 Change User.avatar to be Optional[Asset] instead of Asset
This change was needed to allow users to more easily check if an
uploaded avatar was set using `if user.avatar:` rather than the
admittedly clunky `if user.avatar != user.default_avatar.

The old behaviour with a fallback is still useful for actual display
purposes, so it has been moved over to the new `User.display_avatar`
attribute. This also has symmetry with the newly added
`Member.display_avatar` attribute.
2021-08-25 01:43:09 -04:00
Rapptz
78598d59d7 Change on_socket_raw_receive to dispatch right before JSON conversion 2021-08-25 01:26:37 -04:00
Rapptz
ef79305961 Exclude private functions from VoiceClient 2021-08-24 22:49:46 -04:00
Ryan
c6a6c6af85
Add Colour.brand_green and Colour.brand_red 2021-08-24 07:02:36 -04:00
Rapptz
9a61a5a063 Typehint Colour.__init__ parameter 2021-08-24 03:15:28 -04:00
Rapptz
565b41b0b2 Fix Embed.from_dict typing being too strict for a public function
The Embed TypedDict is not publicly accessible so would always lead
to type errors upon usage.
2021-08-24 03:15:06 -04:00
Sebastian Law
835432d161
Allow enums to be compared 2021-08-24 02:28:39 -04:00
Imayhaveborkedit
f586f4dfbd
Clarify connect() requires Intents.voice_states 2021-08-24 00:02:21 -04:00
Rapptz
769db38401 Fix typing error in sticker edit 2021-08-23 23:52:45 -04:00
Rapptz
c82739a3be Fix some typings in HTTPClient to not take strict payload types 2021-08-23 23:52:16 -04:00
Rapptz
8306b9f6af Add type hint for suppress parameter 2021-08-23 23:51:40 -04:00
Rapptz
490bbffc93 Remove in-place edits and return fresh instances instead
Fixes #4098
2021-08-23 23:46:50 -04:00
Rapptz
9d4fa0341e Fix typing of move role position payload parameter 2021-08-23 23:44:20 -04:00
Rapptz
cff9ca0288 Fix typings for member HTTP methods 2021-08-23 23:44:20 -04:00
Rapptz
9dd86bbcb3 Add type hints to AsyncWebhookAdapter methods 2021-08-23 23:44:20 -04:00
Rapptz
8bbb8f6db9 Use getattr for default_auto_archive_duration in Message.create_thread
Some channel types do not have this attribute so a fallback is
necessary to prevent the attribute access from erroring.
2021-08-23 23:44:20 -04:00
Stocker
d8b06ca7f2
Fix message.py typehints 2021-08-23 23:43:57 -04:00
Imayhaveborkedit
3561ce9d5a
Fix FFmpeg based audiosource input piping
Due to an oversight that has existed since the very beginning, the pipe
argument has been broken since there was nothing to actually write 
the data to the process's stdin.  Now there is.

Also josh made me add typings blegh
2021-08-23 21:05:31 -04:00
Stocker
ae01a96bef
Add missing type: ignore and missing typehint to channel.py 2021-08-23 21:03:56 -04:00
MrKomodoDragon
5ef37923de
Make getters in Client positional only 2021-08-23 05:28:52 -04:00
James Hilton-Balfe
61abb43b69
Fix type hints for decorators in utils 2021-08-23 05:26:30 -04:00
MrKomodoDragon
73f953eac5
Add missing return type to utils.oauth_url 2021-08-23 05:25:21 -04:00
Stocker
400936df69
Fix type for content param in HTTPClient.send_message 2021-08-23 05:24:43 -04:00
Stocker
cdf46127ae
Added type: ignores where needed to activity.py 2021-08-23 05:22:36 -04:00
apple502j
851dfc3c75
Fix permissions.py typing 2021-08-23 01:57:58 -04:00
Rapptz
b8898c7788 Fix user.py typings and reformat file 2021-08-22 07:38:07 -04:00
Rapptz
d17551f51f Fix typing for private channel by user lookup 2021-08-22 07:36:39 -04:00
Rapptz
4a6670c062 Add missing typehint for PermissionOverwrite.pair
Technically inferred but better to be explicit
2021-08-22 07:29:30 -04:00
Rapptz
d7a4230007 Fix Member.guild_avatar docstring 2021-08-22 07:07:26 -04:00
Rapptz
2e52059555 [types] Add avatar to Member typings 2021-08-22 06:55:53 -04:00
Rapptz
49cf959784 Fix Member._avatar not updating in member update events 2021-08-22 06:54:38 -04:00
JustAnyone
91652e3b60
Add per-guild member avatar support
Fix #7054
2021-08-22 06:49:42 -04:00
apple502j
9db8698748
Typehint opus.py 2021-08-22 06:08:30 -04:00
Josh
9727b56503
Fix references in docs 2021-08-22 05:50:28 -04:00
apple502j
e46d974c8a
Typehint oggparse.py 2021-08-22 05:44:41 -04:00
Josh
d09993d7e7
Remove created_at from Snowflake Protocol 2021-08-22 05:28:10 -04:00
Rapptz
69f578abdc Fix webhook typings and use PartialMessageable instead of Object 2021-08-22 04:32:15 -04:00
Rapptz
1b5c206279 Fix broken rename from pyright 2021-08-22 03:05:22 -04:00
Rapptz
d2dd31de63 Make __main__ template strings private 2021-08-22 02:42:08 -04:00
Rapptz
d0c295b595 Add explicit type annotation to version_info and reformat 2021-08-22 02:38:51 -04:00
Rapptz
e1e3e298b5 Typehint async_context global variable 2021-08-22 02:37:40 -04:00
Rapptz
4a72201617 Make json conversion functions private 2021-08-22 02:35:58 -04:00
Rapptz
ea2d972666 Make global log variable in modules private 2021-08-22 02:33:51 -04:00
Rapptz
6268cad402 ResponseType type alias is private 2021-08-22 02:28:37 -04:00
Riley Shaw
55f79ed096
Add typing metadata 2021-08-22 02:25:13 -04:00
Stocker
4065014794
Add type: ignore for StageInstance.channel 2021-08-21 16:28:15 -04:00
Rapptz
8d80259a80 Reformat shard.py 2021-08-21 14:53:19 -04:00
Rapptz
311eac97b0 Reformat state.py 2021-08-21 14:48:22 -04:00
Rapptz
d5033b04a2 Don't clear views in READY 2021-08-21 14:47:08 -04:00
Stocker
7592300535
Typehint state.py 2021-08-21 14:39:02 -04:00
Josh
166152647c
[commands] Make GroupMixin Generic 2021-08-21 14:35:05 -04:00
Arnav Jindal
6c36df6c11
Add missing typehints for Member properties 2021-08-21 14:18:27 -04:00
Rapptz
fbc4a51c35 Fix on_typing not dispatching for threads 2021-08-20 20:09:18 -04:00
Rapptz
9246bbc8e3 [commands] De-indent after hook call
Close #7412
2021-08-20 20:08:27 -04:00
Willy
fa5a2188bb
Copy docs from Client.close() to Bot.close() 2021-08-20 20:06:51 -04:00
Stocker
5390caa67d
Typehint shard.py 2021-08-20 20:05:02 -04:00
Josh
745cf541ea
Re-define Member properties inferred from User to support type-checking 2021-08-20 20:02:39 -04:00
Stocker
ef32f6d882
Typehint context_managers.py 2021-08-20 19:50:39 -04:00
Stocker
b6590d7f56
Add a few typehints to opus.py 2021-08-19 20:23:35 -04:00
Stocker
b5a717fb80
Fix missing typehint that causes an error for a type checker 2021-08-19 20:19:24 -04:00
Stocker
f4d5fcc8f9
Add Thread to the return type of Client.get_channel
Also explains some type ignores.
2021-08-19 20:18:27 -04:00
Sebastian Law
1d2eaf8526
[commands] reset view when Optional argument encounters parsing error 2021-08-19 19:56:28 -04:00
Josh
f3cb197429
[commands][types] Type hint commands-ext 2021-08-19 19:51:26 -04:00
Nadir Chowdhury
d4c683738d
default to 0 instead of 15 for Guild.sticker_limit 2021-08-19 06:21:52 -04:00
James Gayfer
489e5f3288
Use channel default auto archive duration
Currently creating a new thread uses a default auto archive duration of
1440 minutes, or 1 day.

Rather than prescribing our own default, we can use the default auto
archive duration that is set on the channel. This ensures that newly
created threads will respect the default auto archive duration as
prescribed by the user.
2021-08-18 05:12:40 -04:00
Rapptz
63434fbfd9 Fix some type hints in user.py 2021-08-18 01:59:06 -04:00
Rapptz
68453c7bed Add Thread.members and Thread.fetch_members 2021-08-18 01:58:16 -04:00
Rapptz
b73f02b9c3 Remove deprecated TextChannel.active_threads for Guild version
This also fills in the ThreadMember data from the endpoint
2021-08-18 01:52:24 -04:00
Rapptz
0df3f51a0b Partially type-hint state.py
This is just work to get started. A more complete type hint will come
later.
2021-08-18 01:52:24 -04:00
Rapptz
17f0b59c76 Move explanation note for type ignore above the offending line 2021-08-18 01:24:15 -04:00
Rapptz
28ed599345 Fix Template.source_guild typehint 2021-08-18 01:23:17 -04:00
Stocker
e79a648987
Add missing typehints to template.py 2021-08-18 01:17:20 -04:00
Bryan Forbes
79bae47992
flag_value should not be a generic class
Since there is no generic information in `flag_value.__init__()`, 
`flag_value` descriptors get stored as `flag_value[<nothing>]` in mypy
strict mode and then the `__get__` overloads never match. This leads to
errors when using things like `permissions_instance.embed_links` since
`<nothing>` never matches `Permissions`. 

The generic inheritance isn't needed at all since the type information
we care about comes from the call site of `__get__` and not the 
instantiation of the descriptor.
2021-08-18 01:05:08 -04:00
Stocker
8fdd1f0d8f
[commands] Typehinted errors.py 2021-08-18 01:03:44 -04:00
Rapptz
3b4c6269be Fix documentation for Message.is_system
Also fixes some formatting
2021-08-18 01:01:24 -04:00
z03h
27debe18ca
Update Permissions classmethods to include thread/stage/sticker 2021-08-18 00:46:30 -04:00
la
8ac5cdc314
Fix Guild.fetch_channel not working for threads 2021-08-18 00:45:45 -04:00
AkshuAgarwal
6b6bcb92e6
Fix missing or broken versionadded in docstrings
This also documents BadFlagArgument.flag
2021-08-18 00:45:02 -04:00
SYCKGit
f7a3ea90b8
Add other message types exclusions to Message.is_system
Message.is_system was checking if self.type is MessageType.default
but now there are other MessageTypes that are not system messages
2021-08-18 00:42:48 -04:00
thetimtoy
c4ee9dcafa
[commands] Return removed cog in Bot.remove_cog
The method now returns the removed cog, if it exists.
2021-08-18 00:39:54 -04:00
thetimtoy
529fad6fec
Type-hint user.py 2021-08-18 00:37:33 -04:00
Miolus
36b9bc8ee3
Add interaction.data to docs 2021-08-18 00:25:30 -04:00
Stocker
6587b5c7ea
Typehint raw_models.py 2021-08-18 00:23:16 -04:00
Rapptz
feae059c68 Remove coverage note from the documentation and README
A lot of people seem to point out this line as a gotcha when certain
features haven't been released yet. It's been more of a pain-point
than a marketing win since people seem to be unaware of the way this
project is developed.

Fix #7401
2021-08-15 14:01:59 -04:00
Rapptz
1e17b7fcea Rename start_thread to create_thread for consistency 2021-08-12 00:30:50 -04:00
Rapptz
fda543c844 Fix incorrect indent 2021-08-11 06:25:22 -04:00
Rapptz
08a4db3961 Revert "Refactor Client.run to use asyncio.run"
This reverts commit 6e6c8a7b2810747222a938c7fe3e466c2994b23f.
2021-08-11 02:16:22 -04:00
Rapptz
6e6c8a7b28 Refactor Client.run to use asyncio.run
This also adds asynchronous context manager support to allow for
idiomatic asyncio usage for the lower-level counterpart. At first
I wanted to remove Client.run but I figured that a lot of beginners
would have been confused or not enjoyed the verbosity of the newer
approach of using async-with.
2021-08-10 23:00:24 -04:00
David
f631ed22b6
Clarify StageInstance.discoverable_disabled documentation 2021-08-10 22:13:10 -04:00
MrKomodoDragon
a9d9f496f0
Add missing ] in docs for StageInstance.channel 2021-08-10 22:12:36 -04:00
Rapptz
dc9c224b54 Undo coercion of partial DMChannel to PartialMessageable 2021-08-10 22:10:45 -04:00
Rapptz
1c40d43fd1 Remove unused log lines in HTTPClient 2021-08-10 09:28:54 -04:00
Rapptz
66871f329e Interaction.channel can be a PartialMessageable rather than Object
This allows it to work just fine in DMs
2021-08-10 09:28:14 -04:00
Rapptz
1279510194 Add support for PartialMessageable instances
This allows library users to send messages to channels without fetching
it first.
2021-08-10 09:24:49 -04:00
Gnome!
4fca699810
Fill in ConnectionState.user via HTTPClient.static_login 2021-08-10 08:37:29 -04:00
Josh
5e800a726e
[types] Add Application Command Type payloads 2021-08-10 08:36:37 -04:00
monoue
1418464813
[docs] Fix typo 2021-08-10 08:35:31 -04:00
Josh
1c63816cc0
[commands] Document / type-hint cooldown 2021-08-10 08:35:15 -04:00
Aaron Hennessey
ec32b71ff9
[commands] Document GuildNotFound 2021-08-10 08:34:11 -04:00
z03h
c628224403
[commands] Add GuildStickerConverter 2021-08-10 08:31:20 -04:00
Rapptz
58ca9e9932 Add TextChannel.default_auto_archive_duration 2021-08-02 04:36:02 -04:00
Rapptz
0cc67e58ed Fallback to None message_id searches in View dispatch
Not all persistent views have an associated message_id attached to
them.

Fix #7319
2021-08-01 03:30:45 -04:00
Rapptz
035d110837 Fix debug event toggle not triggering for raw receive 2021-08-01 02:42:11 -04:00
Rapptz
b87d306a70 Remove unused variable in view 2021-08-01 02:40:11 -04:00
Rapptz
e795d341e7 Change View dispatch mechanism to be keyed by message_id as well
If different persistent view instances are used within different
message_ids their callbacks will get called without differentiating
between them, leading to potential issues such as 404 errors. This
change makes it so N views with custom IDs bound to N message_ids
will no longer conflict with one another.
2021-07-31 23:08:05 -04:00
Rapptz
f72350199d Fix typo in TextChannel.start_thread 2021-07-31 22:55:32 -04:00
Rapptz
b640493300 Add enable_debug_events parameter to enable expensive debug events
This should allow less dispatching during heavy gateway event streams.
2021-07-31 20:35:28 -04:00
Rapptz
2de0398d66 Cast removed thread member IDs to int 2021-07-31 20:22:51 -04:00
Rapptz
e2250d402e Add on_socket_event_type event 2021-07-31 20:12:40 -04:00
Rapptz
2cb5ce981e FIx on_thread_member_remove passing in None 2021-07-31 19:43:05 -04:00
Rapptz
41f3998a08 Fix on_thread_member_remove not dispatching 2021-07-31 19:40:47 -04:00
LightSage
13a47dfe6e
Fix docs for stickers 2021-07-31 19:40:41 -04:00
Willy
8b148afabd
Update description of auto_archive_duration kwarg of Thread.edit 2021-07-31 19:40:17 -04:00
Rapptz
3a451c9c65 Change payload to use sticker_ids instead of sticker_items 2021-07-30 23:19:25 -04:00
Rapptz
655bf25cc8 Document TypeError raise in lottie sticker 2021-07-30 23:18:03 -04:00
Rapptz
6beef898c6 Rename instances of nitro to premium 2021-07-30 23:10:36 -04:00
Rapptz
658b61d468 Fix SyncWebhook not working across thread barriers
Fix #7310
2021-07-30 22:59:23 -04:00
nickofolas
06a371e80a
Fix User.accent_color incorrect attribute access 2021-07-30 21:30:54 -04:00
James
608e3b5b6c
Rename types.Union -> UnionType for bpo-44732 2021-07-30 21:30:25 -04:00
marshall
d08725b0f0
[commands] Fix NSFW check within threads under NSFW channels 2021-07-30 21:30:10 -04:00
Willy
3ad95f3746
[commands] Document dynamic_cooldown 2021-07-30 21:29:40 -04:00
SYCKGit
56f800de9c
Add Thread to Messageable docstring 2021-07-30 21:27:24 -04:00
Arthur
8db79d2579
Add Thread.category 2021-07-30 21:27:10 -04:00
z03h
8851e03a6d
[commands] fix bot_has_role and is_nsfw for threads 2021-07-30 21:26:49 -04:00
Nadir Chowdhury
60d82cf908
implement guild stickers 2021-07-30 21:25:41 -04:00
Rapptz
ecf239d2a2 Fix user cache acting incorrectly with evictions
The first issue involved copied users which would lead to user updates
causing faster evictions of the cache than was expected.

The second issue involved users that weren't bound to an internal
lifetime eviction policy. These users would not get evicted.
For example, a user without mutual guilds or being part of the internal
cache in general (messages, DMs) would never end up being evicted for
some strange reason. To handle this case, store_user would get a
counterpart named create_user which would create a user without
potentially storing them in the cache. That way only users with a
bound lifetime within the library would be stored.
2021-07-29 01:43:23 -04:00
Rapptz
0d3bd3083c Add Guild.get_channel_or_thread helper method
The name might change in the future, unsure.
2021-07-29 01:22:07 -04:00
Rapptz
731a8816bb [commands] Check for ctx.guild instead of abc.GuildChannel 2021-07-28 23:53:24 -04:00
Rapptz
dac0267e28 Allow creating a public thread without a starter message 2021-07-28 23:50:02 -04:00
Rapptz
13251da8ce Use a default value for StageInstance.discoverable_enabled
Apparently this caused crashes
2021-07-28 20:02:45 -04:00
Rapptz
9d25bb454b Fix recursive inheritance in BaseUser 2021-07-28 20:01:44 -04:00
nickofolas
0112c2819f
Fix PartialMessage rejecting Threads 2021-07-28 20:00:43 -04:00
Nadir Chowdhury
906c13d4f0
Set Thread.member/message_count 2021-07-28 20:00:30 -04:00
Lilly Rose Berner
a053f77275
Add system_content for thread message types, fix other system_content issues 2021-07-28 20:00:14 -04:00
PythonCoderAS
51b02f2568
Change type to be compatible with the overwrites property 2021-07-28 19:59:38 -04:00
Lucas Hardt
ca9b371982
Add support for discord.ui.Select.disabled 2021-07-28 19:58:39 -04:00
Josh
41e2d3c637
Allow callables in abc.Connectable.Connect 2021-07-28 19:58:01 -04:00
Nadir Chowdhury
154c90ef59
Add audit log events for threads 2021-07-28 19:57:42 -04:00
Stocker
1472e9ed7c
Fixes minor grammatical error in Command.update 2021-07-28 19:56:36 -04:00
AkshuAgarwal
85f3e11ef1
Fix Example Usage in docs 2021-07-28 19:56:12 -04:00
Gnome!
67026809a8
Fix EqualityComparable.__eq__ typing 2021-07-28 19:55:19 -04:00
xPolar
dd8168f902
Fix a typo within the documentation 2021-07-28 19:54:58 -04:00
Nadir Chowdhury
96b9a0e09d
Add reason kwarg to more methods 2021-07-28 19:54:32 -04:00
Josh
c059d43e98
Add Number type (10) to Application Command Option types 2021-07-28 19:53:58 -04:00
Robin5605
78aea51f50
Add an example for the new dropdowns 2021-07-28 19:53:06 -04:00
Alex Nørgaard
b47133dfb2
Add BaseUser.banner for all subclasses to access new banners 2021-07-28 19:41:43 -04:00
Ay355
fc51736b34
Add a new view example for link buttons 2021-07-22 10:02:42 -04:00
Willy
23852404ed
Fix incorrect typehint in send_message 2021-07-21 02:52:51 -04:00
AXVin
980d6abbea
Fix typo in commands documentation 2021-07-21 02:52:33 -04:00
Vaskel
bc75945088
Add format_dt to utils __all__ 2021-07-21 02:52:06 -04:00
z03h
dd5fc656d9
Fix permissions_for for roles 2021-07-21 02:51:46 -04:00
thetimtoy
8675a18185
[commands] Remove unused copy import in Cog 2021-07-21 02:48:11 -04:00
scrazzz
a0e5e062c9
Add versionadded to ui.View and ui.Item 2021-07-21 02:47:52 -04:00
Sebastian Law
07483297ad
Fix typo in UserFlags.has_unread_urgent_messages 2021-07-21 02:47:36 -04:00
z03h
48eb981344
Fix Thread.slowmode_delay not updating 2021-07-21 02:47:15 -04:00
Kaylynn Morgan
feed302269
Fix documentation note for interaction_check 2021-07-21 02:46:52 -04:00
Nadir Chowdhury
262a50196d
fix typo in ephemeral function definition 2021-07-21 02:46:39 -04:00
Aaron Hennessey
15eb3d2e5d
Remove afk parameter from change_presence 2021-07-21 02:45:57 -04:00
Ryu JuHeon
0faf4c8e2b
Stricter type hint in releaselevel 2021-07-21 02:45:21 -04:00
Lilly Rose Berner
5b8be9a772
Add PartialMessage to list of allowed message reference types 2021-07-21 02:45:01 -04:00
Steve C
834e23dc00
Fix type annotations for purge's limit param on Thread/TextChannel
Optional was missing.
2021-07-21 02:44:27 -04:00
Alex Nørgaard
26e68b31ef
[commands] fix incorrect typings in ThreadConverter 2021-07-21 02:43:37 -04:00
Nadir Chowdhury
f14e584304
Sync Guild.features to documentation 2021-07-10 03:13:14 -04:00
Nadir Chowdhury
e2624b9a31
[commands] Fix (Partial)MessageConverter to work with thread messages 2021-07-10 03:11:34 -04:00
Rapptz
f153154b7a Undocument Item.width
It doesn't serve much of a purpose to users
2021-07-09 09:14:01 -04:00
Rapptz
1a4e73d599 [commands] Get guild_id from message link for message converters 2021-07-09 09:14:01 -04:00
Lilly Rose Berner
0aa825557d
Re-try requests on 504 error and raise correct error 2021-07-09 04:54:22 -04:00
Rapptz
8fb998b599 Refactor utcfromtimestamp to use fromtimestamp(..., tz=utc) 2021-07-09 04:20:45 -04:00
Rapptz
826ce101fd Change WEBHOOK_UPDATE to use guild information from gateway
This changes the lookup from unnecessary O(n) to two amortised O(1)
lookups. This event pretty much always has a guild_id so the original
code was always a performance bottleneck.
2021-07-08 21:53:26 -04:00
Rapptz
b4e39668fb Change CHANNEL_PINS_UPDATE to use guild information from gateway
This changes the channel lookup from O(n) to two amortised O(1) lookups
2021-07-08 21:51:21 -04:00
Rapptz
af8742a911 Use a specific tag type for member and user comparisons
The previous protocol based tag type caused significant overhead
(in the magnitude of seconds). Removing this should simplify object
creation by removing typing.Generic from the __mro__
2021-07-08 10:17:44 -04:00
Rapptz
8df35c83a9 Remove extraneous dict assignment from view store 2021-07-07 20:19:17 -04:00
Rapptz
74e1ab09a0 Remove channel type coercion in factory methods
This caused unnecessary isinstance checks which were slowing down
channel creation at scale
2021-07-07 20:19:17 -04:00
Rapptz
03cd6ff433 Remove calls to gc.collect in ConnectionState
For some bots this was actually a performance bottleneck, might as
well remove it if it causes bad performance
2021-07-07 20:19:17 -04:00
Rapptz
99b8ae35ba Change _Overwrite to get dict entries rather than mutate 2021-07-07 20:19:17 -04:00
Rapptz
cb2363f0fd Move global user storage from WeakValueDictionary to dict
Profiling showed that WeakValueDictionary caused rather significant
and noticeable slowdowns during startup. Since the only thing it was
used for was to automatically remove the key from the mapping when
the reference count reaches zero, the same could theoretically be
accomplished by using the __del__ special method. There is a chance
that this could lead to a memory leak since the __del__ method is not
always called, but the only instances of this happening are during
interpreter shutdown to my knowledge and at that point the mapping
is the least of my concern.
2021-07-07 20:19:17 -04:00
Rapptz
88d825a803 Allow use of orjson instead of json
The difference in speed seems negligible at start up, which is when
most time is taken for actually parsing JSON. I could potentially be
missing something but profiling didn't point to any discernable
difference.
2021-07-07 20:19:17 -04:00
Josh
e0a9365d61
Type-hint backoff.py 2021-07-07 20:17:17 -04:00
apple502j
717e723a36
Update intents docs to reflect presence update changes 2021-07-07 20:16:17 -04:00
z03h
1ca5b7b8b2
[commands] update clean_content to work when standalone 2021-07-07 20:15:54 -04:00
ToxicKidz
3cb539d91b
[commands] Document the thread converter 2021-07-07 20:15:18 -04:00
Alex Nørgaard
5a7cfb3ce6
[commands] Add ThreadConverter 2021-07-05 00:40:57 -04:00
Rapptz
c1c6457598 Separate member_update and presence_update events 2021-07-04 21:40:30 -04:00
Rapptz
c2acb0a114 Update README to point to 3.8 requirement 2021-07-04 21:26:27 -04:00
Rapptz
feb0f7f29d Add SelectOption.__str__ 2021-07-04 21:20:48 -04:00
Aaron Hennessey
7598865609
Fix typo in stage_instance.py 2021-07-04 19:14:33 -04:00
Josh
f40f80c0dc
Fix typo in voice_client.py 2021-07-04 19:14:07 -04:00
Rapptz
49f8073262 Fix Permission serialisation in audit logs
Fix #7171
2021-07-04 19:13:21 -04:00
Rapptz
750ba88f2c Fix typing errors with Client 2021-07-04 07:55:20 -04:00
Rapptz
074f34a5fa Specify View timeouts is in seconds 2021-07-04 07:07:38 -04:00
Rapptz
7d9074db8a Fix type errors and potentially unbound code in http.py 2021-07-04 06:18:34 -04:00
Rapptz
4152819a3c Ignore linting error when accessing Python 3.10 unions 2021-07-04 05:55:29 -04:00
Rapptz
5d798aa5e6 Fix some typing errors and reformat enums.py 2021-07-04 05:32:26 -04:00
Rapptz
828e47d83f Document on_thread_join also applying to thread creation 2021-07-04 04:55:12 -04:00
Rapptz
23a69144b6 Add a warning in on_interaction 2021-07-04 03:06:36 -04:00
Rapptz
d047cebc35 [commands] Remove window reassignment when tokens reach 0 2021-07-04 03:05:06 -04:00
Rapptz
c748e4bce5 Mention ephemeral messages can only be edited with raw method 2021-07-03 21:45:15 -04:00
Alex Nørgaard
d1dc41ec2f
Fix Client.fetch_channel not returning Thread 2021-07-03 21:35:31 -04:00
Rapptz
097b6064f1 Fix ui.Button constructor default style to match the decorator 2021-07-03 21:29:28 -04:00
Rapptz
17268c3368 Add MessageFlags.ephemeral 2021-07-03 21:29:28 -04:00
Alex Nørgaard
6a553b2347
Fix building docs due to missing InteractionMessage in __all__ 2021-07-03 13:24:57 -04:00
Rapptz
93cc1bdd79 Fix typing errors in PartialEmoji 2021-07-03 11:52:38 -04:00
Rapptz
8b4dd34328 Document TextChannel.start_thread return type 2021-07-03 11:52:20 -04:00
Rapptz
3d0dd5bc1b Change Message.__repr__ to show inherited type name 2021-07-03 11:02:32 -04:00
Rapptz
0b577fa209 Add support for fetching the original interaction response message 2021-07-03 11:00:48 -04:00
Rapptz
be5603141e Remove slots from flags
Fix #7159
2021-07-03 10:52:35 -04:00
Rapptz
bba4d6c4e4 Fix typo with exception name in InteractionResponse 2021-07-03 08:49:07 -04:00
Rapptz
9f981e718b Mention the discord.ui types in the read-only component types 2021-07-03 08:39:02 -04:00
Rapptz
88620d052a Typehint permissions 2021-07-03 08:30:27 -04:00
Rapptz
8760b01e76 Add Interaction.permissions to get resolved permissions 2021-07-03 07:07:54 -04:00
Rapptz
12e90f9c6d Type hint instance variables in interactions 2021-07-03 06:56:30 -04:00
Rapptz
a8db8546db Typehint error.py 2021-07-03 00:54:36 -04:00
Rapptz
0fae0b4995 Use "raised" instead of "thrown" for exception documentation 2021-07-03 00:35:21 -04:00
Rapptz
7ca90874b9 Raise an exception if an interaction has been responded before
Fix #7153
2021-07-03 00:30:32 -04:00
Alex Nørgaard
b7b75e2b1f
Add Thread.is_nsfw 2021-07-03 00:14:48 -04:00
Alex Nørgaard
ffa0b26b82
Fix versionadded on ChannelType.private_thread 2021-07-02 22:05:27 -04:00
jack1142
1059c02df7
Update examples of interactions to mention components 2021-07-02 21:58:06 -04:00
Rapptz
d7ed884593 Rework view timeouts to work as documented 2021-07-02 09:17:32 -04:00
Rapptz
a3d7e06f25 [commands] Add back CommandOnCooldown.type 2021-07-02 05:39:54 -04:00
Rapptz
982140b5f7 [commands] Mention that dynamic_cooldown callable can return None 2021-07-02 05:35:38 -04:00
Rapptz
69c400d813 Add Thread.mention 2021-07-01 20:51:56 -04:00
Rapptz
9ac459b5d3 Add a default style for buttons
This makes it easier to create URL buttons since the library will
automatically assign the proper style for it.
2021-07-01 20:45:38 -04:00
Rapptz
4f0e907e44 Add ButtonStyle.url alias for ButtonStyle.link 2021-07-01 20:42:56 -04:00
Rapptz
812bfbe6f9 Show Select.values more prominently in the documentation 2021-07-01 20:33:57 -04:00
Alex Liu
64b48431b4
Add type property to thread channels 2021-07-01 20:27:39 -04:00
jack1142
30605e6f4f
Add Select to list of types in Item's docstring 2021-07-01 07:50:04 -04:00
Nadir Chowdhury
2d597e310b
Fix Interaction.channel being None in threads 2021-07-01 07:49:44 -04:00
Josh
d001b9d0ee
[docs] Fix more references
Co-Authored-By: Riley Shaw <30989490+ShineyDev@users.noreply.github.com>
2021-07-01 07:48:37 -04:00
Rapptz
ed6c061d69 [commands] Fix guild channel converters to work in DMs
Fix #7147
2021-07-01 07:46:02 -04:00
Rapptz
12e3eba011 Add Select to the docs 2021-06-30 03:54:30 -04:00
Rapptz
c1f1c67eed Change timeout parameter in View.from_message to keyword only 2021-06-30 03:15:45 -04:00
Arnav Jindal
cd4b0904db
Change NamedTuple in __init__.py 2021-06-30 03:12:40 -04:00
Rapptz
d8075d5412 Add View.from_message to convert message components to a View 2021-06-30 03:06:51 -04:00
Rapptz
157caaec7c Add conversion routine for SelectMenu to ui.Select 2021-06-30 02:55:03 -04:00
Rapptz
62dad0f7bf Fix potential None access in various StageChannel properties 2021-06-30 01:31:57 -04:00
Rapptz
1aeec34f84 Typehint Member and various typing fixes 2021-06-30 01:28:35 -04:00
Rapptz
44d1d29708 Add explicit types to variables in Message types 2021-06-29 23:56:02 -04:00
Rapptz
ea1d423904 Check for None in VocalGuildChannel.voice_states 2021-06-29 23:16:39 -04:00
Rapptz
6f3b8072d6 Rework User.edit to have proper typing 2021-06-29 22:05:29 -04:00
Rapptz
2d7c709235 Rework Role.edit to not rely on previous role state 2021-06-29 22:02:16 -04:00
Rapptz
a372aadb2d Rework Member.edit to not use kwargs for better typing 2021-06-29 21:54:24 -04:00
Rapptz
53ef89c29f Use a property for StreamIntegration.expire_behavior alias 2021-06-29 21:46:19 -04:00
Rapptz
c6a69062a8 Rework StreamIntegration.edit to not rely on state 2021-06-29 21:45:06 -04:00
Rapptz
62b024803a Mock PartialTemplateState._get_guild as pass-through 2021-06-29 21:35:00 -04:00
Rapptz
b1a355394f Rework Template.edit to use MISSING sentinel 2021-06-29 21:33:57 -04:00
Josh
39a674ddee
Fix typing of ApplicationCommandInteractionDataOption 2021-06-29 21:29:24 -04:00
Nadir Chowdhury
abac04b759
Fix link buttons not being regarded as persistent 2021-06-29 20:03:50 -04:00
Nadir Chowdhury
7d0bd7ed20
add persistent view in on_ready to avoid loop issues 2021-06-29 20:02:55 -04:00
Josh
7601d6cec3
[typing] Type-hint client.py 2021-06-29 20:02:19 -04:00
Rapptz
7386a971f8 Add examples for how to use views 2021-06-29 04:19:12 -04:00
Rapptz
2beee8be14 Type hint channel.py 2021-06-29 03:37:52 -04:00
Rapptz
eda02c2e91 [types] VoiceChannel and StageChannel bitrate/user_limit is not null 2021-06-29 02:45:01 -04:00
Rapptz
485542c480 Fix typing linting error in threads 2021-06-29 02:26:07 -04:00
Rapptz
a2b10a08b9 Fix KeyError due to refactoring mistake in Overwrite handling 2021-06-28 23:51:56 -04:00
Rapptz
55c7de82d3 Type and format abc.py
There's still some stuff missing but this is a decent first pass
2021-06-28 23:36:20 -04:00
Rapptz
f9bccabac5 Make Asset.with_ functions positional only 2021-06-28 23:36:20 -04:00
Rapptz
d9adf4d35d Make Asset.replace only accept keyword arguments 2021-06-28 23:36:20 -04:00
Rapptz
f7d551953b Remove extraneous __slots__ assignments 2021-06-28 23:36:20 -04:00
Lilly Rose Berner
6b1d46a1ea
Set Message.guild from guild_id if unavailable through Message.channel 2021-06-28 19:03:59 -04:00
Rapptz
e96df33ce0 Dispatch thread_join when a thread is updated but not in cache 2021-06-28 18:56:28 -04:00
Rapptz
e3a66bcccc Fix property CSS to be more inline with everything else 2021-06-28 04:16:51 -04:00
Rapptz
039bb9f871 Move documentation CSS after CSS block 2021-06-28 03:44:22 -04:00
Rapptz
2ce44f7bd6 Add versionadded for format_dt 2021-06-28 03:36:16 -04:00
scrazzz
3c5c5fc274
Update documentation in voice_client.py 2021-06-28 03:10:33 -04:00
Rapptz
d1a2ee4620 Add discord.utils.format_dt helper function 2021-06-28 01:31:14 -04:00
Rapptz
a75cd93acc Fix Guild.vanity_invite causing an error when guild has it unset
FIx #7103
2021-06-28 01:03:46 -04:00
Josh
5acea453cc
Type-hint voice_client / player 2021-06-28 00:59:14 -04:00
Rapptz
cd6b453cb3 Typehint Activity 2021-06-28 00:56:28 -04:00
Rapptz
4566b64d77 Fix Activity and Spotify datetime being timezone naive 2021-06-28 00:37:16 -04:00
Rapptz
b1836c5577 Rework Message.edit implementation 2021-06-28 00:33:59 -04:00
RobotHanzo
75477b2995
Fix incorrect typehints in Guild.create_role 2021-06-28 00:05:40 -04:00
Izhar Ahmad
2cd2d1d3ee
[commands] Rename missing_perms to missing_permissions 2021-06-28 00:05:08 -04:00
Nadir Chowdhury
f7b0ed7b12
Add ButtonStyle.gray alias 2021-06-28 00:01:13 -04:00
Lilly Rose Berner
b59ec318c0
Fix Category.create_x_channel raising without overwrites 2021-06-28 00:00:51 -04:00
thegamecracks
6ce1c537d4
Localize Embed.timestamp during assignment 2021-06-28 00:00:17 -04:00
pikaninja
caa9512a8a
Make on_ready examples consistent 2021-06-27 23:59:17 -04:00
Aomi Vel
47e6a754e4
Add support for sending multiple embeds 2021-06-27 23:52:48 -04:00
quiprr
8b7e5a50b4
Add discord.Spotify.track_url 2021-06-27 23:48:04 -04:00
Harmon
1a3422dccc
Handle role_id possibly being None for StreamIntegration 2021-06-27 23:46:14 -04:00
Josh
233d10649c
[docs] Update Sphinx and Fix various references
Co-Authored-By: Riley Shaw <30989490+ShineyDev@users.noreply.github.com>
2021-06-27 23:43:49 -04:00
NiumXp
76c9e390f1
remove repeat 'to' in Task.restart doc 2021-06-27 23:42:43 -04:00
Rafael
cbe7a1b3a2
Add "new in version" missing in webhook documentation 2021-06-27 23:41:00 -04:00
Devon R
b2c9c26841
Show decorator usage instead of signature in docs 2021-06-27 23:40:39 -04:00
Steve C
20dd632722
Fix Member.ban typing to include 0-day message deletes 2021-06-27 23:38:29 -04:00
thetimtoy
3c2cf06e46
[commands] Add attr and parameter "argument" to BadInviteArgument 2021-06-27 23:38:07 -04:00
Soheab
dbb135b81a
Add disable_guild_select to utils.oauth_url() 2021-06-27 23:37:02 -04:00
Rapptz
d30fea5b0d Add changelog for v1.7.3 2021-06-12 12:28:08 -04:00
Rapptz
f27e2e073f Fix crash involving stickers 2021-06-12 11:56:51 -04:00
Rapptz
0bc5f276a7 [commands] Change EmojiConverter to use Client.get_emoji 2021-06-12 03:05:28 -04:00
Rapptz
1c640ad72b Lazily create Button custom_ids in decorator interface
The previous code would make two separate instances share the custom_id
which might have been undesirable behaviour
2021-06-12 02:33:11 -04:00
Rapptz
f0c76a13d3 Fix guild documentation not showing up for some methods 2021-06-11 05:25:06 -04:00
Josh
a0e1d1e25f
Fix typing of IntegrationAccount class 2021-06-11 04:25:23 -04:00
Rapptz
04573c3c06 Make View timeout parameter keyword-only 2021-06-10 09:06:04 -04:00
Rapptz
f6ea03230e Make parameters passed to Reaction.user keyword-only 2021-06-10 08:58:25 -04:00
Rapptz
c251c51cb1 Typehint Reaction 2021-06-10 08:58:10 -04:00
Rapptz
9181bf046b Rename Reaction.custom_emoji to Reaction.is_custom_emoji
This legacy attribute was apparently never changed to be consistent
with the rest of the library
2021-06-10 08:50:50 -04:00
Josh
04788d0a06
Type-Hint appinfo/team 2021-06-10 08:06:00 -04:00
Rapptz
fc66c5b92d Fix some webhook related type checker errors 2021-06-10 07:57:41 -04:00
Rapptz
0dd4c4c08c Don't use class attribute syntax for Guild typings 2021-06-10 07:50:35 -04:00
Josh
35a9533e8d
Type-Hint http.py 2021-06-10 07:34:41 -04:00
Rapptz
11e23c534a Close ClientSession after closing websocket connections 2021-06-10 07:32:05 -04:00
Devon R
ee26b58c6c
None check in InteractionResponse.edit_message 2021-06-10 07:28:26 -04:00
Lilly Rose Berner
fa6fa6a567
Add category_id shortcut to Thread 2021-06-09 08:21:45 -04:00
Lilly Rose Berner
2eb0ec07ab
Add __str__ method to Thread 2021-06-09 08:21:14 -04:00
Nadir Chowdhury
c2df574b2a
Add audit log entries for stage instances 2021-06-09 08:09:05 -04:00
Rapptz
7dccbace78 Refactor Guild to support type hints
This patch also does the following:

* Sets some parameters to be positional only
* Changes Guild.edit to use the MISSING sentinel
* Changes the various create_channel methods to be type safe
* Changes many parameters from Optional[T] to use MISSING
* Changes Guild.create_role to use MISSING sentinel

This refactor is mostly partial but lays a decent foundation
2021-06-08 10:56:26 -04:00
Rapptz
2247fbb23a [types] Use proper type for Guild.threads 2021-06-08 09:28:29 -04:00
Rapptz
c693945a46 [types] Split PartialVoiceState for proper gateway type 2021-06-08 09:27:55 -04:00
Rapptz
746da7d54c Add Thread.permissions_for helper function 2021-06-08 07:29:17 -04:00
Rapptz
1152f67efc Allow pins events to work with threads 2021-06-08 07:29:17 -04:00
Rapptz
5ae7940ec8 Add message purging functions to Thread 2021-06-08 07:29:17 -04:00
Rapptz
e13cbf4644 Don't dispatch thread_join on extraneous THREAD_CREATE dispatches 2021-06-08 07:29:17 -04:00
Rapptz
bd369c76ea Parse remaining thread events. 2021-06-08 07:29:17 -04:00
Rapptz
9adf94e6b1 Add ThreadMember.thread 2021-06-08 07:29:17 -04:00
Rapptz
92ee2cd598 Add support for thread parameter in Webhook.send 2021-06-08 07:29:15 -04:00
Rapptz
4b51e3e253 Add TextChannel.active_threads 2021-06-08 07:26:22 -04:00
Rapptz
d0d2d7ea62 Clarify actions that require manage_threads permission 2021-06-08 07:26:22 -04:00
Rapptz
5a72391b72 Add thread related permissions 2021-06-08 07:26:22 -04:00
Rapptz
3a421a3eb9 Add TextChannel.get_thread shortcut helper 2021-06-08 07:26:22 -04:00
Rapptz
b2176dc0ef Change how threads are created
Instead of start_public_thread and start_private_thread they'll now be
one method.

I might revert this if starting a public thread without a message never
ends up happening.
2021-06-08 07:26:22 -04:00
Rapptz
40127eb7b5 Fix import error with threads archived iterator 2021-06-08 07:26:22 -04:00
Rapptz
b9d8d3872e Add __repr__ for the thread classes 2021-06-08 07:26:22 -04:00
Rapptz
429c5933d9 Add minor parsing for THREAD_LIST_SYNC and THREAD_MEMBER_UPDATE
There's no dispatch for these yet
2021-06-08 07:26:22 -04:00
Rapptz
a16f54afdb Replace Ellipsis with utils.MISSING 2021-06-08 07:26:22 -04:00
Rapptz
a09f89cedf Fix partial thread members 2021-06-08 07:26:22 -04:00
Rapptz
c6d09a8bfa Add Thread.is_news() 2021-06-08 07:26:22 -04:00
Rapptz
72c66a1706 Bump gateway API to v9 2021-06-08 07:26:22 -04:00
Rapptz
4a4e73ec14 Update thread typings and payloads to match documentation 2021-06-08 07:26:18 -04:00
Rapptz
ac95b8b85b Allow Message.channel to be a thread 2021-06-08 07:25:40 -04:00
Rapptz
51cc7622a6 TextChannel.archived_threads is not a coroutine 2021-06-08 07:25:40 -04:00
Rapptz
cb9a506686 Fix typo with archived_threads iterator leading to AttributeError 2021-06-08 07:25:40 -04:00
Rapptz
7c6724fdd7 Fix typo in start_private_thread
This also renames archive_threads to archived_threads
2021-06-08 07:25:40 -04:00
Rapptz
9d3962aa7a [types] Fix some minor ordering mishap on MessageType 2021-06-08 07:25:40 -04:00
Rapptz
c1ce3b949f Implement remaining HTTP endpoints on threads
I'm not sure if I missed any -- but this is the entire documented set
so far.
2021-06-08 07:25:30 -04:00
Rapptz
68c7c538f5 First pass at preliminary thread support
This is missing a lot of functionality right now, such as two gateway
events and all the HTTP CRUD endpoints.
2021-06-08 07:23:40 -04:00
Rapptz
6c79714b42 [types] Add support thread API typings 2021-06-08 07:13:19 -04:00
UP929312
4724943861
Grammatical improvements in View documentation 2021-06-08 06:54:10 -04:00
Rapptz
5c2945bcd4 Fix AttributeError in is_nsfw() methods 2021-06-08 05:37:10 -04:00
Nadir Chowdhury
94bbdc154c
update types subpackage with latest docs 2021-06-07 23:20:47 -04:00
Nadir Chowdhury
a7ae2eb1bb
Add Guild.nsfw_level 2021-06-07 23:20:04 -04:00
Arnav Jindal
dd727fb6f4
Add Embed.remove_footer 2021-06-07 03:33:11 -04:00
Nadir Chowdhury
ab6d592f8c
Add support for integration create/update/delete events 2021-06-07 03:28:26 -04:00
TheLeadingLlama
2ea2693bd7 Add the Guild.delete_custom_emoji method 2021-06-07 03:25:09 -04:00
Lilly Rose Berner
fb0c6c56e1
Return message content for replies in Message.system_content 2021-06-06 17:31:32 -04:00
Rapptz
81e9d70b7b Add pre-conditions to avoid on_timeout being called after stop()
Apparently the cancellation request for a TimerHandle doesn't
necessarily have to be honoured despite large periods of time passing
2021-06-06 07:05:17 -04:00
Rapptz
876b1e0f3e Add View.on_error callback for swallowed exceptions 2021-06-05 08:22:44 -04:00
Rapptz
27556ea0a2 Fix DM channel permissions not having read_messages 2021-06-05 02:53:30 -04:00
Rapptz
dbd9ed2c41 Add View.is_dispatching to detect whether a view has been added 2021-06-04 04:30:19 -04:00
Aman Kumar
9e4bcd3df7
Fix some typos in custom_context example
Also adds PyNaCl PyPI link in README
2021-06-02 06:31:58 -04:00
Rapptz
4b1059579e Fix NameError in missing _EmojiTag import 2021-06-02 06:00:39 -04:00
Rapptz
47f2d04940 Allow passing Emoji in components 2021-06-02 05:30:42 -04:00
Rapptz
be5f4ae4ab Properly type hint attributes in Emoji 2021-06-02 05:30:42 -04:00
xPolar
2f0a2b244e
Update documentation for on_voice_state_update
The event also gets triggered by stage channels.
2021-06-02 04:03:53 -04:00
MhmCats
0847085661
Add support for editing guild widgets 2021-06-02 02:39:08 -04:00
James
369951fd80
Typehint audit_logs.py 2021-06-02 02:28:47 -04:00
Rapptz
bac6c2fc7b [commands] Unwrap functions to get their module and globalns
Fixes #7002
2021-06-01 08:51:35 -04:00
Rapptz
78275023cc Add Client.persistent_views to get all persistent views 2021-05-31 23:15:12 -04:00
Rapptz
7c40e83d10 Ensure views added to Client.add_view are persistent views 2021-05-31 23:08:08 -04:00
Rapptz
c811932ca7 Don't mark URL buttons as dispatchable 2021-05-31 22:57:44 -04:00
Rapptz
09f0ed1fba Mention that rows are explicitly 0 indexed. 2021-05-31 20:19:08 -04:00
Rapptz
89d24cb0bc Add interaction enums to __all__ 2021-05-31 05:56:50 -04:00
Rapptz
d0097c4281 Remove view syncing before editing in views
This prevents a potential race condition when a MESSAGE_UPDATE is
received syncing and refreshing the view components causing a desync.
2021-05-31 05:50:40 -04:00
Rapptz
4a3491cc0a Check for view finished state before resuming listening on edit 2021-05-31 00:18:06 -04:00
Rapptz
8dafe4f544 Add support for editing in views in PartialMessage 2021-05-31 00:17:35 -04:00
Rapptz
2ed3e049e1 Add View.is_finished() to query listening state 2021-05-31 00:12:08 -04:00
Rapptz
61a189c217 Sync views in InteractionResponse.edit_message 2021-05-31 00:09:15 -04:00
Nadir Chowdhury
9f98a9a87f
Implement StageInstance 2021-05-30 13:51:52 -04:00
Rapptz
90a28d48d5 Fix potential KeyError when removing views 2021-05-30 12:29:46 -04:00
Rapptz
7b1c57ed60 Add support for interaction followups 2021-05-30 11:25:15 -04:00
Rapptz
2ebd5315f9 Add support for sending and editing views in Webhook 2021-05-30 11:25:00 -04:00
Rapptz
c9cdb47338 Add __repr__ for View 2021-05-30 11:24:23 -04:00
Rapptz
db58e628ba Allow Webhook.send to send ephemeral messages
This is only available for application webhooks
2021-05-30 10:29:29 -04:00
Rapptz
267fad9180 Add WebhookType.application 2021-05-30 10:02:58 -04:00
Rapptz
c6f3ed1af4 Allow sending View with Interaction.response.send_message
This also allows for ephemeral views and listening to said views
2021-05-30 06:10:58 -04:00
Rapptz
1b15772671 Allow assigning Select.options to refresh the select menu 2021-05-30 03:20:29 -04:00
Rapptz
02c317d9a4 Fix Message.edit typings to take View parameters 2021-05-30 00:00:24 -04:00
Rapptz
7bd1211b36 Rework item grouping behaviour to take into consideration weights
This also renames `group` into `row`
2021-05-29 23:58:37 -04:00
Rapptz
695662416a Fix Messageable.send overload to take view parameters 2021-05-29 08:01:37 -04:00
Rapptz
c21d12be5e Check future state before setting result in View 2021-05-29 05:52:05 -04:00
Rapptz
d78e5d979d Refactor and type hint invites 2021-05-29 05:49:19 -04:00
Rapptz
5a68d3a561 Typehint AllowedMentions 2021-05-29 04:26:21 -04:00
Rapptz
5a9cbc967b Typehint mixins 2021-05-29 02:44:31 -04:00
Rapptz
794327cdb4 Fix type errors with required keys in the integration types 2021-05-29 01:09:07 -04:00
Rapptz
1ae40a11b7 Fix some type errors in StreamIntegration.edit 2021-05-29 00:54:31 -04:00
Rapptz
06743dd434 Make StreamIntegration.role a property rather than a strong reference 2021-05-29 00:50:56 -04:00
Rapptz
732c5384fd Allow registering a View for persistent long term dispatching 2021-05-29 00:44:08 -04:00
Maya
4d7822493f
Add support for bot integrations 2021-05-29 00:43:33 -04:00
Rapptz
7e1f8bf1b4 Typehint Sticker 2021-05-29 00:19:11 -04:00
Michael H
52678b2eb5
[commands] Add Command.extras 2021-05-29 00:18:02 -04:00
Zomatree
b48f510e15
Add invite targets for voice channel invites 2021-05-29 00:15:46 -04:00
Rapptz
f321efd4de Default SelectOption.value to the label if not given 2021-05-28 09:43:15 -04:00
Rapptz
b84c199c70 Allow constructing SelectOption.emoji from a string as well 2021-05-28 09:40:49 -04:00
Rapptz
c475218112 Typehint Role and RoleTags 2021-05-28 08:54:45 -04:00
Rapptz
35bef7af38 Fix Role.is_assignable() computing Guild.me twice 2021-05-28 08:41:45 -04:00
Rapptz
f4fe247813 Remove __slots__ from View 2021-05-28 08:39:34 -04:00
TheOneMusic
9ba5745e68
Check for guild owner in Role.is_assignable() 2021-05-28 08:18:49 -04:00
Rapptz
ef9f61a933 Add support for select components 2021-05-28 05:34:21 -04:00
Rapptz
6874aa73c4 Add PartialEmoji.from_str helper 2021-05-28 05:34:21 -04:00
Rapptz
ff36aedf7b Add support for reading SelectMenu components from messages 2021-05-28 05:34:21 -04:00
Rapptz
8bd17ede47 Move ActionRow to its own separate type split from Component 2021-05-28 02:11:10 -04:00
Rapptz
aeb2cfb573 Add private get_slots utility to get slots through MRO 2021-05-28 01:56:48 -04:00
Rapptz
263f45d05b Fix View.wait not returning when it times out
This also makes it so it returns the reason why the wait finished.
2021-05-28 00:53:28 -04:00
Rapptz
3f60997630 Add a timeout callback for detecting when a View times out 2021-05-28 00:53:28 -04:00
Rapptz
97f308d219 Add View.remove_item and View.clear_items 2021-05-28 00:53:28 -04:00
Rapptz
3453992ce6 Add View.interaction_check for interaction pre-conditions 2021-05-28 00:53:28 -04:00
Tyler
6c8f1ccbdf
Add Role.is_assignable() 2021-05-28 00:53:23 -04:00
Rapptz
65db814d4a Add a way to wait for a view to finish its interactions 2021-05-27 23:31:48 -04:00
MrKomodoDragon
77ed476129
Fix extraneous colons in the documentation for ButtonStyle 2021-05-27 22:34:31 -04:00
Zomatree
6cc3e572ba
Button labels can be None 2021-05-27 22:33:13 -04:00
Michael H
1bf782fcb5
Add Member.get_role
Adds an efficient way to check if a member has a role by ID.

 This is done in a way consistent with the existing user API of the
 library.

 The more debated Member.has_role_id/has_role is intentionally not
 included for review at this time given the heavy bikeshedding of it.
2021-05-27 22:31:49 -04:00
Rapptz
1954861668 Add warning for comparing with role positioning 2021-05-27 21:11:17 -04:00
Rapptz
fc64ffdabd Allow passing multiple embeds in InteractionResponse.edit_message 2021-05-27 21:07:28 -04:00
Rapptz
fbafe20e51 Allow View to be instantiated without subclassing 2021-05-27 01:41:18 -04:00
Rapptz
c89882441c Fix typings for resolved channels in slash commands 2021-05-27 00:53:14 -04:00
Rapptz
7584834dd4 Only automatically defer if no response was given in callback 2021-05-27 00:53:14 -04:00
Rapptz
3b83f60b35 Add support for setting interaction responses 2021-05-27 00:53:14 -04:00
Rapptz
85758a75b3 Add interaction related endpoints to async webhook 2021-05-27 00:53:14 -04:00
Rapptz
d42c63e186 Fix some type hints in interactions 2021-05-27 00:53:14 -04:00
AXVin
2ad2cab50c [Interactions] Create User only when in DMs 2021-05-27 00:53:14 -04:00
Rapptz
5e96ad9261 Force button style to link if a URL is passed 2021-05-27 00:53:14 -04:00
Rapptz
80fd222ca0 Add aliases for button style colours 2021-05-27 00:53:14 -04:00
Rapptz
eda6680377 Rename enums to use official API naming 2021-05-27 00:53:14 -04:00
Rapptz
cc800796a2 Properly guard for DMs in interaction creation
Fix #6794
2021-05-27 00:53:14 -04:00
Rapptz
ed9badcddf Make Item and Button generic over the underlying view 2021-05-27 00:53:14 -04:00
Rapptz
4c0ebc9221 Change the way callbacks are defined to allow deriving
This should hopefully make these work more consistently as other
functions do.
2021-05-27 00:53:14 -04:00
Rapptz
cc56f31bcd Fix emoji not showing up in button component 2021-05-27 00:53:13 -04:00
Rapptz
98570793e4 Add initial support for buttons and components 2021-05-27 00:53:13 -04:00
Mikey
f42e922696
Fix bug in Embed.__len__ caused by footer without text 2021-05-27 00:45:35 -04:00
Rapptz
f56543df15 [commands] Remove function call indirection when checking author 2021-05-25 20:53:44 -04:00
Stanisław Jelnicki
67aabc3230
Remove VerificationLevel aliases 2021-05-25 20:51:08 -04:00
Tari
36cf3c94b4
[commands] Remove Bot.self_bot 2021-05-25 20:46:26 -04:00
numbermaniac
3b55573777
Fix minor typo in typing() docs 2021-05-25 02:23:14 -04:00
Imayhaveborkedit
ac061c31fb
Fix default hook signature
Since the hook function can be both bound and unbound
the bound signature needs to accept an extra argument
2021-05-25 02:22:21 -04:00
Rapptz
3c90f16bf0 Fix cached_slot_property typings again 2021-05-23 21:30:44 -04:00
pikaninja
3cb093c709
Add a note about overwriting in set_permissions 2021-05-23 05:08:15 -04:00
Stanisław Jelnicki
65439732b3
Add Discord Certified Moderator user flag 2021-05-23 05:07:33 -04:00
Nadir Chowdhury
f87eaa613d
[docs] typo fix 2021-05-23 03:47:16 -04:00
apple502j
5acb3a62f8
Fix Webhook example 2021-05-23 03:42:37 -04:00
Imayhaveborkedit
8e08bd6af2
Add vws message hook 2021-05-23 03:42:07 -04:00
MrKomodoDragon
cc8a86a4bd
Improve the example for abc.Messageable.typing 2021-05-23 03:37:55 -04:00
Cryptex
71fe40aafa
Consistent loop attribute description 2021-05-23 03:36:20 -04:00
Tari
42bab370a7
[commands] Add BadColorArgument to __all__ 2021-05-16 15:40:06 -04:00
Rapptz
81b259ab36 Fix sending arrays with nulls in them when changing presences 2021-05-16 07:15:53 -04:00
chromacoat dreamkey
c896563af4
Fix Colour.fuchsia docstring typo 2021-05-15 20:00:35 -04:00
Dorukyum
5ad88dec72
Change Colour.blurple to new one
This moves the old one to Colour.og_blurple.
2021-05-15 02:13:12 -04:00
Josh
42a538edda
[tasks] Replace None check with MISSING check in task loop 2021-05-15 02:10:00 -04:00
NextChai
ef6f5d947a
[commands] Update command.parent and command.parents docs
* Switch root_parent from command to group
2021-05-15 02:09:37 -04:00
Alex Nørgaard
fb20c4c3d4
Update docs for (Partial)Message.publish to reflect the actual permissions needed 2021-05-15 02:08:16 -04:00
Arnav Jindal
ee3e2944ba
Add Colour.fuchsia and Colour.yellow 2021-05-15 02:07:45 -04:00
ChasL
9d114fb066
Fix for doc reference to python "raise" statement
:ref:`py:raise` -> :ref:`raise statement <py:raise>`

Before fix the text reads: "...define an on_error handler consisting
of a single empty The raise statement." After fix it should read: 
"...define an on_error handler consisting of a single empty raise
statement."
2021-05-15 02:06:50 -04:00
Stanisław Jelnicki
9b4e820bbe
Document Invite.inviter as optional 2021-05-12 20:34:44 -04:00
Josh
5fa64e83e0
Fix issues with imports causing NameErrors 2021-05-12 20:24:28 -04:00
Sebastian Law
124c4a3919
Add Template.url 2021-05-12 06:38:26 -04:00
Josh
ef22178dee
[tasks] Type hint the tasks extension 2021-05-12 06:31:40 -04:00
Sebastian Law
f5727ff0d0
[tasks] fix regular task loops 2021-05-10 20:25:16 -04:00
Nadir Chowdhury
757cfad38f
Type up **kwargs of various methods 2021-05-10 20:24:48 -04:00
Sebastian Law
8bc489dba8
[tasks] Add support for explicit time parameter 2021-05-09 23:27:43 -04:00
Josh
8b2241916a
Typehint Widget 2021-05-09 23:22:12 -04:00
Jay3332
d5e14eb715
[commands] Fix a minor grammar error in MaxConcurrencyReached 2021-05-07 07:38:44 -04:00
sudosnok
2a6d79078e
[commands] Add GuildChannelConverter 2021-05-07 07:37:42 -04:00
Josh
7ebfface22
Explicitly ignore legacy file reference errors in sphinx -n mode 2021-05-06 09:12:19 -04:00
Rapptz
de965c2bf5 Simplify SnowflakeList type hints 2021-05-06 08:23:18 -04:00
Rapptz
2e12f6de9c Typehint File 2021-05-06 08:15:51 -04:00
Josh
3864fb37a0
Fix various reference issues in documentation
Co-Authored-By: Riley Shaw <30989490+ShineyDev@users.noreply.github.com>
2021-05-06 07:51:07 -04:00
Rapptz
1bf7aadf94 Typehint emoji classes 2021-05-05 23:48:36 -04:00
Rapptz
7bad27d215 Fix SnowflakeList typings 2021-05-05 23:33:11 -04:00
Rapptz
ca92f37f18 Fix typings in message.py 2021-05-05 23:05:15 -04:00
Rapptz
81004369dc Add Guild.fetch_channel 2021-05-05 14:26:33 -04:00
Rapptz
598057ee79 Add Permissions.manage_events 2021-05-05 14:16:39 -04:00
Rapptz
c31946f29f Type hint GuildChannel and don't make it a Protocol
This reverts GuildChannel back into a base class mixin.
2021-05-05 11:14:58 -04:00
Josh
7fde57c89a
Type-hint object.py 2021-05-05 10:29:07 -04:00
Josh
c69b20c52c
Type hint colour.py 2021-05-05 10:27:52 -04:00
Josh
6622be9f46
Make GuildChannel inherit Snowflake 2021-05-05 07:42:39 -04:00
Nadir Chowdhury
63974ec46d
Add discovery_splash and community field to Guild.edit 2021-05-05 07:30:54 -04:00
Rapptz
b32ad3de37 Fix AuditLogEntry.target being incorrect for bulk message delete
Fixes #6851
2021-05-04 22:03:20 -04:00
Sebastian Law
b82a0dc6fd
[docs] remove mentions of bot only usability 2021-05-04 07:21:59 -04:00
pikaninja
1d4e339141
Add get_user to the things intents.members affects 2021-05-03 22:15:28 -04:00
Josh
dc67d2bd85
Replace uses of Ellipsis as sentinels with utils.MISSING 2021-05-03 00:31:07 -04:00
Rapptz
e3037b32d5 Add changelog for v1.7.2 2021-05-02 23:39:21 -04:00
NoName
2793fc06d5
Clarify Webhook.send return value documentation 2021-05-02 18:21:11 -04:00
Rapptz
cd7357b93a Add classproperty helper utility
This is apparently a 3.9+ only addition
2021-05-02 08:05:52 -04:00
Rapptz
b0ec22065e Add Client.create_dm 2021-05-01 13:16:57 -04:00
Rapptz
83611edb66 Fix supressing messages leading a 400 error
This only makes it so allowed_mentions are passed if the message is
authored by the bot itself.
2021-05-01 11:46:27 -04:00
Rapptz
135a7e9e5a Reformat message.py file 2021-05-01 09:54:19 -04:00
Rapptz
d940486552 Add types to PartialMessage 2021-05-01 09:48:37 -04:00
Rapptz
4c97b8efdd Fix typings for utils._parse_ratelimit_header
A CIMultiDict is not a Dict but a Mapping
2021-05-01 09:24:47 -04:00
Rapptz
60c1240849 Fix SyncWebhook exception case causing attribute errors 2021-05-01 09:24:40 -04:00
Rapptz
02e21a8905 Fix sending multipart data with SyncWebhook
Fixes #6825
2021-05-01 09:21:39 -04:00
Zomatree
3381d1e089
Add typings for message related classes 2021-05-01 08:51:13 -04:00
Nadir Chowdhury
e762f55847
Add fetch_invite with with_expiration 2021-05-01 07:46:16 -04:00
Joey van Langen
4fcbe75d3b
Fix guild application command endpoints 2021-05-01 07:44:23 -04:00
Nadir Chowdhury
51df7496db
Add AuditLogChanges.rules_channel/public_updates_channel 2021-05-01 06:47:22 -04:00
Nadir Chowdhury
b705173676
Add types for ApplicationCommandPermissions & co 2021-05-01 06:46:26 -04:00
David
66b17f5afb
Clarify ClientUser.verified docs 2021-04-30 19:12:40 -04:00
MrKomodoDragon
a8945b5784
Fix grammar in the Guild.edit docstring 2021-04-30 19:08:45 -04:00
Rapptz
1a8d63d54f [commands] Remove Flag related delimiter and prefix error 2021-04-30 03:29:44 -04:00
Rapptz
58274eafbc [commands] Fix Generics causing other typing converters to fail 2021-04-30 01:23:56 -04:00
Rapptz
7b8db49efe Fix utils.MISSING not evaluating to True in implicit bool contexts 2021-04-30 01:05:01 -04:00
Rapptz
3b6a2b9e85 [commands] Fix Generic subcalsses used as a converter 2021-04-29 02:43:54 -04:00
Nadir Chowdhury
cbbd31cc9f
fix AttributeError in Sticker.image 2021-04-29 02:16:55 -04:00
Nadir Chowdhury
c786a85a9b
Add utils.MISSING 2021-04-29 01:58:36 -04:00
Olivia
a2df6e81b8
[commands] Update error message for Literal float/complex 2021-04-27 22:04:18 -04:00
pikaninja
56f4ae3a83
[docs] Update notes for get_user and get_member 2021-04-27 21:56:00 -04:00
apple502j
127b3239e9
Fix AttributeError in examples 2021-04-27 08:09:01 -04:00
Rapptz
9f3551926a Split annotation resolution to discord.utils 2021-04-27 05:48:27 -04:00
Rapptz
69da87f455 [commands] Disallow float/complex in Literal but allow None
Type checkers (both mypy and pydantic) apparently don't like it
2021-04-27 05:48:27 -04:00
numbermaniac
b84717fc76
Make spelling of "colour" consistent in docs 2021-04-27 04:37:35 -04:00
ppotatoo
f4a861d76e
Add __int__ to discord.Colour 2021-04-26 21:38:20 -04:00
HyperGH
686e531624
Adjust quickstart to not show commands example 2021-04-26 21:37:44 -04:00
Josh
3c2674725a
Add as_chunks helper function 2021-04-25 23:36:03 -04:00
Rapptz
d60689a983 Properly change abc.User.avatar type to Asset 2021-04-25 23:30:30 -04:00
Josh
20c2664a50
[docs] Remove extraneous icon definition 2021-04-25 19:21:09 -04:00
Nadir Chowdhury
1765cdffb1
Use Asset for AuditLogChanges and add more entries 2021-04-25 09:53:38 -04:00
Josh
368fda7272
Remove HypesquadHouse enum from docs 2021-04-25 04:35:45 -04:00
Josh
4fbc78ba81
[commands] Add support for typing.Union to Flags 2021-04-25 04:35:19 -04:00
Rapptz
c250b9fc02 [commands] Fix regression with Union converters not working
This was due to the Literal restriction from earlier.
2021-04-24 09:33:35 -04:00
Rapptz
185b554a56 [commands] Disallow complicated Literal types 2021-04-24 09:12:28 -04:00
Rapptz
fb024546ff [commands] Fix Literal converter not working within flags 2021-04-24 08:55:55 -04:00
Rapptz
1c312a158a [commands] Add FlagConverter.__iter__ 2021-04-24 08:53:36 -04:00
Nadir Chowdhury
829c2d4a1a
Add Activity.buttons 2021-04-24 00:27:47 -04:00
Stella
91c473db57
[commands] Fix _HelpCommandImpl.clean_params popitem 2021-04-23 02:24:09 -04:00
Rapptz
8e9860077d [commands] Fix flag detection code in get_flags 2021-04-23 02:23:07 -04:00
Alex Nørgaard
e09f64b7c9
Fix typo in FlagConverter docs 2021-04-22 21:12:19 -04:00
Josh
e5607822d3
Define utils.cached_property in if TYPE_CHECKING guard 2021-04-22 21:11:56 -04:00
Rapptz
275a754abd Add support for editing message attachments 2021-04-21 23:45:06 -04:00
sudosnok
67abfea61a
Add target_user and target_type to Invite objects 2021-04-21 23:30:35 -04:00
Nadir Chowdhury
f4165755a9
Rename lingering _url Asset properties 2021-04-21 23:24:36 -04:00
jack1142
a55e817ffe
Fix documentation for RoleConverter 2021-04-21 23:24:04 -04:00
Nadir Chowdhury
157801bc90
Add Template.is_dirty 2021-04-21 23:22:22 -04:00
Josh
8457f70477
[commands] Set constructible FlagConverter flags to not be required 2021-04-21 23:21:02 -04:00
Nadir Chowdhury
1d7f387122
[docs] stage_channels doc typo 2021-04-21 23:18:49 -04:00
Josh
cfe93f19b1
[commands] Allow FlagCommand subclasses to inherit options 2021-04-21 07:16:23 -04:00
Josh
42463bae67
[commands] Add support for aliasing to FlagConverter 2021-04-21 00:31:01 -04:00
Kino
0c1c9284f6
Fix typo within HelpCommand.verify_checks documentation 2021-04-21 00:18:32 -04:00
Rapptz
6065329c0e [commands] Avoid creating unnecessary flag mapping copies 2021-04-20 08:02:40 -04:00
Rapptz
15bfdf66b2 [commands] Default construct flags if they're not passed as parameters
This only applies if and only if the flag can be default constructible.
Ergo, all the flags are optional or not required.
2021-04-20 08:00:47 -04:00
Rapptz
ac7588f735 Fix some typings in utils 2021-04-20 04:35:53 -04:00
Rapptz
212d308835 [commands] Some minor clean up of the flag converter documentation
Fix #6761
2021-04-19 22:52:29 -04:00
Nadir Chowdhury
7bfb0f8133
[docs] fix docstring of AppInfo 2021-04-19 22:32:07 -04:00
Arnav Jindal
2e6c28bd60
Bump Python version in Quickstart documentation 2021-04-19 22:26:56 -04:00
Rapptz
18bf3d3a7d [commands] Actually expose the FlagError base error 2021-04-19 10:27:24 -04:00
Rapptz
ddb71e2aed [commands] Initial support for FlagConverter
The name is currently pending and there's no command.signature hook
for it yet since this requires bikeshedding.
2021-04-19 10:25:08 -04:00
Rapptz
1c64689807 Remove lingering User.avatar documentation 2021-04-19 05:35:33 -04:00
Rapptz
c54e43360b [commands] Add run_converters helper to call converters 2021-04-19 04:46:02 -04:00
Rapptz
09f3f2111c [commands] Add Context.current_parameter 2021-04-19 04:41:32 -04:00
Rapptz
3c8d3ab078 Add a third overload to parse_time
Apparently this behaviour is intended in Pyright
https://github.com/microsoft/pyright/issues/1772

Despite mypy behaving as intended.
2021-04-19 02:12:41 -04:00
Rapptz
bee6402d84 Fix utils.find predicate typing to accept Any 2021-04-19 02:11:38 -04:00
Cryptex
8d74fad474
Update lavalink's repo url 2021-04-18 23:12:05 -04:00
Nadir Chowdhury
95777230b0
Add MessageType.guild_invite_reminder 2021-04-18 20:33:56 -04:00
Nadir Chowdhury
631a0b1e13
Add support for ApplicationFlags 2021-04-18 20:32:52 -04:00
Rapptz
417353da4d Fix up _unique and valid_icon_size implementations 2021-04-18 09:07:26 -04:00
Rapptz
b35596f7c8 Add typings for discord.utils 2021-04-18 08:43:09 -04:00
Rapptz
cc4dced7c0 Cleanup some of the prior typings for cached_slot_property 2021-04-18 08:07:11 -04:00
Nadir Chowdhury
18badbc60f
Add typing for utils.cached(_slot)_property 2021-04-18 07:56:38 -04:00
Rapptz
7fb746e6e5 [commands] Refactor evaluation functions to allow passing in localns 2021-04-18 04:10:19 -04:00
Zomatree
aac0374baf
Add privacy policy and tos fields to AppInfo 2021-04-17 23:52:46 -04:00
Nadir Chowdhury
821b6c61cb
[docs] document inherited members on Asset 2021-04-17 23:40:13 -04:00
Rapptz
c2afa984ff Use f-strings for attributetable 2021-04-17 19:40:40 -04:00
Rapptz
fdf81089b5 Add inherited members to (Partial)Emoji docs 2021-04-17 19:40:40 -04:00
Nadir Chowdhury
e4513f70ad
activities is no longer nullable 2021-04-17 18:56:51 -04:00
Steve C
86f10f6dd6
Add missing reprs to some objects
These are WidgetMember, BaseUser, and DeletedReferencedMessage
2021-04-17 18:56:08 -04:00
z03h
304229071f
Add VoiceChannel.video_quality_mode 2021-04-17 08:10:41 -04:00
Rapptz
fed259a83b Refactor save() and read() into AssetMixin 2021-04-17 00:56:38 -04:00
Rapptz
f6fcffbab5 Use default allowed_mentions in Message.edit
Fix #6745
2021-04-16 23:00:18 -04:00
Steve C
ef9bb79e91
[tasks] Move the Loop's sleep to be before exit conditions
This change makes it more so that `Loop.stop()` gracefully makes the
current iteration the final one, by waiting AND THEN returning.
The current implementation is closer to `cancel`, while also not.

I encountered this because I was trying to run a
`@tasks.loop(count=1)`, and inside it I print some text and change the
interval, and in an `after_loop`, I restart the loop.

Without this change, it immediately floods my console, due to
not waiting before executing `after_loop`.
2021-04-16 22:35:18 -04:00
Rapptz
6ba3d89076 Revert Attachment.save code to prior implementation 2021-04-16 11:38:56 -04:00
Rapptz
9eaf1e85e4 Rewrite Asset design
This is a breaking change.

This does the following transformations, assuming `asset` represents
an asset type.

Object.is_asset_animated() => Object.asset.is_animated()
Object.asset => Object.asset.key
Object.asset_url => Object.asset_url
Object.asset_url_as => Object.asset.replace(...)

Since the asset type now requires a key (or hash, if you will),
Emoji had to be flattened similar to how Attachment was done since
these assets are keyed solely ID.

Emoji.url (Asset) => Emoji.url (str)
Emoji.url_as => removed
Emoji.url.read => Emoji.read
Emoji.url.save => Emoji.save

This transformation was also done to PartialEmoji.
2021-04-16 11:27:23 -04:00
Nadir Chowdhury
57dbb37a52
Add fetch_message for webhooks 2021-04-16 11:27:15 -04:00
Nadir Chowdhury
b610998491
Remove Sticker.preview_image 2021-04-16 08:19:39 -04:00
Nadir Chowdhury
5dec62f4c0
[commands] Add a converter for discord.Object 2021-04-16 08:18:57 -04:00
Rapptz
a30ec197c2 Some initial response typings 2021-04-16 08:02:19 -04:00
NoName
74f92387ac
Add periods to sticker docs 2021-04-16 07:51:02 -04:00
Nadir Chowdhury
d3ac191a67
Restrict snowflake regexes to 15-20 digits 2021-04-16 07:33:44 -04:00
Nadir Chowdhury
8f9819eb4c
[docs] Fix various unresolved references 2021-04-15 22:41:41 -04:00
pikaninja
ffea48f218
[commands] Remove HelpCommand.clean_prefix (#6736) 2021-04-15 21:28:08 -04:00
Rapptz
90d59bb06c Fix overloads on Webhook.send to not require wait kwarg 2021-04-15 19:36:36 -04:00
Rapptz
0542b129c2 Fix WebhookMessage.edit documentation 2021-04-15 09:03:46 -04:00
Rapptz
1f74b051a8 Fix rate limit handling with retry_after precision change 2021-04-15 08:34:58 -04:00
Rapptz
a6f7213c89 Rewrite webhooks to play better with typings and rate limits
This unfortunately required splitting the types into two. This led to
a lot of unfortunate code duplication that I didn't really enjoy
writing.

The new design allows users to pass an authentication token to make
webhook requests without the webhook token and allows to finally
edit the webhook channel.

The new design also uses a contextvar to store rate limiting
information so multiple instances or recreating instances no longer
clears the ratelimiting state since it's now essentially a "global"
object.

Closes #6525, closes #6662, closes #2509, closes #1761
2021-04-15 08:04:32 -04:00
Nadir Chowdhury
5ea5f32479
[commands] Fix missing re import in Context 2021-04-15 08:04:24 -04:00
TheOneMusic
d21e65ce47
Add SystemChannelFlags.guild_reminder_notifications 2021-04-15 08:03:56 -04:00
Steve C
65d48302ad
Fix guild.chunk() not working on evicted guilds
If you're trying to chunk a guild that the bot is not in, 
it'll just hang on the chunk coro forever. It's weird, I know.
2021-04-14 22:10:47 -04:00
MrKomodoDragon
ed3c141f5e
[commands] Add clean_prefix attribute to commands.Context 2021-04-14 22:09:40 -04:00
numbermaniac
208b16ed1b
Add note to member docs about Spotify limitation 2021-04-14 22:02:49 -04:00
apple502j
9f1a96ea9b
Remove fetch_offline_members param for Client 2021-04-14 20:58:49 -04:00
Maya
930c416ea7
Fix exception for invalid channel types 2021-04-14 01:14:00 -04:00
Rapptz
da6119e04c Fix fail_if_not_exists not being set when constructed with state 2021-04-14 00:50:37 -04:00
Robin
30310b9ab6
Add NSFW for Guilds 2021-04-14 00:48:51 -04:00
apple502j
dea92a69dc
Remove support for guild subscriptions 2021-04-14 00:47:46 -04:00
pikaninja
23aaa75802
Add StageChannel to abc.GuildChannel docs 2021-04-13 01:19:29 -04:00
Kino
496fcf8005
[docs] Fix reference to Guild.id 2021-04-13 01:01:04 -04:00
Rapptz
a8da7d03b9 Remove AutoShardedClient.request_offline_members 2021-04-12 05:28:18 -04:00
Rapptz
ef4394f87d Add support for role objects in GuildChannel.permissions_for 2021-04-11 22:45:49 -04:00
Rapptz
1209585de5 Remove User.permissions_in
This seemed to only cause confusion.
2021-04-11 22:21:36 -04:00
Rapptz
a8b3cfa592 Remove comment that doesn't apply anymore 2021-04-11 22:20:31 -04:00
Rapptz
9b94fe1ce0 Remove superfluous unused payload parameter 2021-04-11 22:13:48 -04:00
Rapptz
7bdaa793f6 Create temporary DMChannels from message create events
This allows for DMChannels to work without falling back to the
Object error case since there is enough information to build a pseudo
DMChannel object.

This is a breaking change since it changes the type of
DMChannel.recipient to Optional[User] for when this faux object is
created.
2021-04-11 22:09:10 -04:00
Maya
0f3f2cbeea
Fix spelling error in utils.__all__ 2021-04-11 20:18:56 -04:00
Rapptz
74b07a3218 [commands] Fix Command.clean_params to return a regular dict 2021-04-11 16:30:28 -04:00
Kreusada
af5964358d
[commands] Strip text to remove spaces before ellipsis 2021-04-11 15:19:45 -04:00
Nadir Chowdhury
7cbe942a64
Use v8 overwrite type when creating a channel 2021-04-11 15:16:52 -04:00
Nadir Chowdhury
f1fac96e33
Remove private_channel_(delete/create) events 2021-04-11 15:13:23 -04:00
Rapptz
74d8ad2013 [commands] Add support for Python 3.10 Union typing 2021-04-11 02:11:24 -04:00
Rapptz
1ecadf057e [commands] Fix errors with cooldown mappings 2021-04-11 01:00:04 -04:00
Rapptz
7d79b4ba55 Remove Member related handling in PRESENCE_UPDATE 2021-04-11 00:57:59 -04:00
Rapptz
217c2a1cc5 Fix stray AttributeError in Guild._from_data with member cache 2021-04-11 00:44:27 -04:00
Rapptz
40cf397ce6 Permission related fixes for v8 2021-04-11 00:39:13 -04:00
NCPlayz
4c565e5299 add reply and application_command types 2021-04-11 00:39:13 -04:00
Rapptz
7afcacc9a1 Remove MemberCacheFlags.online
v8 no longer gives enough data for this to be possible
2021-04-11 00:39:13 -04:00
Rapptz
d85805ab6d First pass at supporting v8 API 2021-04-11 00:39:13 -04:00
Josh
7f91ae8b67
[commands] use __args__ and __origin__ where applicable 2021-04-11 00:38:17 -04:00
Josh
c54c4cb215
[commands] Fix repr for Greedy 2021-04-10 22:34:24 -04:00
Rapptz
3151672cfe [commands] Refactor typing evaluation to not use get_type_hints
get_type_hints had a few issues:

1. It would convert = None default parameters to Optional
2. It would not allow values as type annotations
3. It would not implicitly convert some string literals as ForwardRef

In Python 3.9 `list['Foo']` does not convert into
`list[ForwardRef('Foo')]` even though `typing.List` does this
behaviour. In order to streamline it, evaluation had to be rewritten
manually to support our usecases.

This patch also flattens nested typing.Literal which was not done
until Python 3.9.2.
2021-04-10 20:56:08 -04:00
Rapptz
27886e5aa4 [commands] Remove legacy ExtensionNotFound.original attribute 2021-04-10 15:52:53 -04:00
Rapptz
d5ad269b35 Fix Intents resolution in the docs 2021-04-10 15:50:46 -04:00
Rapptz
42c3ee6eed Bring back discord module in discord.ext.commands documentation 2021-04-10 15:49:39 -04:00
Rapptz
8e299e80f8 Update Sphinx to 3.5.3 2021-04-10 15:33:15 -04:00
Rapptz
296bd069c1 Remove current module reference in commands API docs 2021-04-10 14:59:26 -04:00
Nadir Chowdhury
b20e92efd8
[docs] Fix references to Greedy 2021-04-10 14:59:09 -04:00
Nadir Chowdhury
353737239a
[commands] Minimise code duplication in channel converters 2021-04-10 14:01:26 -04:00
TheOneMusic
ec71eb2fcb
Added discord.StageChannel in documentation 2021-04-10 13:49:46 -04:00
James
bcd3a00eaf
[commands] Make commands.Greedy a typing.Generic 2021-04-10 07:27:32 -04:00
jack1142
4fee632526
Make the style of external and internal cross-references consistent 2021-04-10 04:03:22 -04:00
jack1142
4591705b55
Add missing attribute tables 2021-04-10 03:42:42 -04:00
Dan Hess
f2d5ab6f80
[commands] Provide a dynamic cooldown system 2021-04-10 03:30:01 -04:00
Rapptz
ea32147d02 Fix all warnings with Sphinx 2021-04-10 03:27:40 -04:00
Rapptz
d58edd10a7 Add missing future annotations import 2021-04-10 03:23:47 -04:00
Nadir Chowdhury
1efdef3ac3
Add typings for invites, templates, and bans 2021-04-10 02:55:10 -04:00
Nadir Chowdhury
3e92196a2b
Add typings for audit logs, integrations, and webhooks 2021-04-10 02:53:24 -04:00
Sigmath Bits
68aef92b37
[commands]Add typing.Literal converter 2021-04-10 02:50:59 -04:00
pikaninja
1952060e1a
make examples on_ready consistent 2021-04-09 18:05:33 -04:00
Cryptex
eb5356cc47
Remove user token warning in login 2021-04-08 23:49:40 -04:00
Rapptz
d38ef88686 Update issue templates to newest versions 2021-04-08 22:51:20 -04:00
Rapptz
ca84d4e2b6 [types] Snowflake can be either str or int. 2021-04-08 22:17:26 -04:00
Sebastian Law
4134a17a29
[commands] Raise error when a cog name is already registered 2021-04-08 22:04:10 -04:00
Rapptz
9da2f349e7 Use Any instead of str for Embed typings for accuracy 2021-04-08 20:12:08 -04:00
Nadir Chowdhury
d4df44375b
Add typings for models for guilds, activities, and voice 2021-04-08 09:32:26 -04:00
Sebastian Law
05c123f3ab
Use f-strings in more places that were missed 2021-04-08 09:31:06 -04:00
Rapptz
99fc950510 Use f-strings in more places that were missed. 2021-04-08 06:02:47 -04:00
N-i-c-k-007
c3e0b6e123
Update joined command in basic_bot to use f-strings 2021-04-08 04:33:42 -04:00
Rapptz
e405bd5f1f Add discord.types.Message.interaction attribute 2021-04-08 02:29:14 -04:00
Rapptz
a31c19563f Add interaction related typings 2021-04-08 02:12:55 -04:00
Rapptz
249d94a011 Add AllowedMentions typings 2021-04-08 02:06:19 -04:00
Rapptz
d299bfef26 Fix up typings for attachment and message 2021-04-08 01:56:23 -04:00
Nadir Chowdhury
0903ea949f
Add typings for Message, Emoji, and Member 2021-04-08 01:51:42 -04:00
Rapptz
e895a53713 [commands] Add StageChannelConverter to documentation 2021-04-08 00:44:47 -04:00
pikaninja
1c553f51fb
[commands] Use has_error_handler instead in command_error 2021-04-07 23:42:52 -04:00
Kallum
cc6edccc0c
Make the bot template use f-strings over str.format 2021-04-07 21:38:01 -04:00
Nadir Chowdhury
f8bea3bb05
Fix inaccuracies with AsyncIterator typings 2021-04-07 20:28:12 -04:00
180 changed files with 28861 additions and 45624 deletions

View File

@ -1,5 +1,7 @@
## Contributing to discord.py
Credits to the `original lib` by Rapptz <https://github.com/Rapptz/discord.py>
First off, thanks for taking the time to contribute. It makes the library substantially better. :+1:
The following is a set of guidelines for contributing to the repository. These are guidelines, not hard rules.
@ -8,9 +10,9 @@ The following is a set of guidelines for contributing to the repository. These a
Generally speaking questions are better suited in our resources below.
- The official support server: https://discord.gg/r3sSKJJ
- The official support server: https://discord.gg/TvqYBrGXEm
- The Discord API server under #python_discord-py: https://discord.gg/discord-api
- [The FAQ in the documentation](https://discordpy.readthedocs.io/en/latest/faq.html)
- [The FAQ in the documentation](https://enhanced-dpy.readthedocs.io/en/latest/faq.html)
- [StackOverflow's `discord.py` tag](https://stackoverflow.com/questions/tagged/discord.py)
Please try your best not to ask questions in our issue tracker. Most of them don't belong there unless they provide value to a larger audience.
@ -32,13 +34,13 @@ If the bug report is missing this information then it'll take us longer to fix t
## Submitting a Pull Request
Submitting a pull request is fairly simple, just make sure it focuses on a single aspect and doesn't manage to have scope creep and it's probably good to go. It would be incredibly lovely if the style is consistent to that found in the project. This project follows PEP-8 guidelines (mostly) with a column limit of 125.
Submitting a pull request is fairly simple, just make sure it focuses on a single aspect and doesn't manage to have scope creep, and it's probably good to go. It would be incredibly lovely if the style is consistent to that found in the project. This project follows the black code format, with a line length limit of `120`
### Git Commit Guidelines
- Use present tense (e.g. "Add feature" not "Added feature")
- Limit all lines to 72 characters or less.
- Reference issues or pull requests outside of the first line.
- Limit all lines to 120 characters or fewer.
- Reference issues or pull requests outside the first line.
- Please use the shorthand `#123` and not the full URL.
- Commits regarding the commands extension must be prefixed with `[commands]`

View File

@ -1,13 +1,12 @@
name: Bug Report
description: Report broken or incorrect behaviour
labels: unconfirmed bug
issue_body: true
body:
- type: markdown
attributes:
value: >
Thanks for taking the time to fill out a bug.
If you want real-time support, consider joining our Discord at https://discord.gg/r3sSKJJ instead.
If you want real-time support, consider joining our Discord at https://discord.gg/TvqYBrGXEm instead.
Please note that this form is for bugs only!
- type: input
@ -73,3 +72,7 @@ body:
required: true
- label: I have removed my token from display, if visible.
required: true
- type: textarea
attributes:
label: Additional Context
description: If there is anything else to say, please do so here.

View File

@ -5,4 +5,4 @@ contact_links:
url: https://github.com/Rapptz/discord.py/discussions
- name: Discord Server
about: Use our official Discord server to ask for help and questions as well.
url: https://discord.gg/r3sSKJJ
url: https://discord.gg/TvqYBrGXEm

View File

@ -1,7 +1,6 @@
name: Feature Request
description: Suggest a feature for this library
labels: feature request
issue_body: true
body:
- type: input
attributes:
@ -44,3 +43,7 @@ body:
What is the current solution to the problem, if any?
validations:
required: false
- type: textarea
attributes:
label: Additional Context
description: If there is anything else to say, please do so here.

View File

@ -1,3 +1,5 @@
<!-- Pull requests that do not fill this information in will likely be closed -->
## Summary
<!-- What is this pull request for? Does it fix any issues? -->

38
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: CI
on: [push, pull_request]
jobs:
pyright:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Setup node.js (for pyright)
uses: actions/setup-node@v1
with:
node-version: "14"
- name: Run type checking
run: |
npm install -g pyright
pip install .
pyright --lib --verifytypes discord --ignoreexternal
black:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Run linter
uses: psf/black@stable
with:
options: "--line-length 120 --check"
src: "./discord"

1
.python-black Normal file
View File

@ -0,0 +1 @@

View File

@ -2,3 +2,4 @@ include README.rst
include LICENSE
include requirements.txt
include discord/bin/*.dll
include discord/py.typed

View File

@ -1,114 +0,0 @@
discord.py
==========
.. image:: https://discord.com/api/guilds/336642139381301249/embed.png
:target: https://discord.gg/nXzj3dg
:alt: Discordサーバーの招待
.. image:: https://img.shields.io/pypi/v/discord.py.svg
:target: https://pypi.python.org/pypi/discord.py
:alt: PyPIのバージョン情報
.. image:: https://img.shields.io/pypi/pyversions/discord.py.svg
:target: https://pypi.python.org/pypi/discord.py
:alt: PyPIのサポートしているPythonのバージョン
discord.py は機能豊富かつモダンで使いやすい、非同期処理にも対応したDiscord用のAPIラッパーです。
主な特徴
-------------
- ``async````await`` を使ったモダンなPythonらしいAPI。
- 適切なレート制限処理
- Discord APIによってサポートされているものを100カバー。
- メモリと速度の両方を最適化。
インストール
-------------
**Python 3.5.3 以降のバージョンが必須です**
完全な音声サポートなしでライブラリをインストールする場合は次のコマンドを実行してください:
.. code:: sh
# Linux/OS X
python3 -m pip install -U discord.py
# Windows
py -3 -m pip install -U discord.py
音声サポートが必要なら、次のコマンドを実行しましょう:
.. code:: sh
# Linux/OS X
python3 -m pip install -U discord.py[voice]
# Windows
py -3 -m pip install -U discord.py[voice]
開発版をインストールしたいのならば、次の手順に従ってください:
.. code:: sh
$ git clone https://github.com/Rapptz/discord.py
$ cd discord.py
$ python3 -m pip install -U .[voice]
オプションパッケージ
~~~~~~~~~~~~~~~~~~~~~~
* PyNaCl (音声サポート用)
Linuxで音声サポートを導入するには、前述のコマンドを実行する前にお気に入りのパッケージマネージャー(例えば ``apt````dnf`` など)を使って以下のパッケージをインストールする必要があります:
* libffi-dev (システムによっては ``libffi-devel``)
* python-dev (例えばPython 3.6用の ``python3.6-dev``)
簡単な例
--------------
.. code:: py
import discord
class MyClient(discord.Client):
async def on_ready(self):
print('Logged on as', self.user)
async def on_message(self, message):
# don't respond to ourselves
if message.author == self.user:
return
if message.content == 'ping':
await message.channel.send('pong')
client = MyClient()
client.run('token')
Botの例
~~~~~~~~~~~~~
.. code:: py
import discord
from discord.ext import commands
bot = commands.Bot(command_prefix='>')
@bot.command()
async def ping(ctx):
await ctx.send('pong')
bot.run('token')
examplesディレクトリに更に多くのサンプルがあります。
リンク
------
- `ドキュメント <https://discordpy.readthedocs.io/ja/latest/index.html>`_
- `公式Discordサーバー <https://discord.gg/nXzj3dg>`_
- `Discord API <https://discord.gg/discord-api>`_

View File

@ -1,65 +1,60 @@
discord.py
==========
enhanced-discord.py
===================
.. image:: https://discord.com/api/guilds/336642139381301249/embed.png
:target: https://discord.gg/r3sSKJJ
.. image:: https://discord.com/api/guilds/514232441498763279/embed.png
:target: https://discord.gg/TvqYBrGXEm
:alt: Discord server invite
.. image:: https://img.shields.io/pypi/v/discord.py.svg
:target: https://pypi.python.org/pypi/discord.py
.. image:: https://img.shields.io/pypi/v/enhanced-dpy.svg
:target: https://pypi.python.org/pypi/enhanced-dpy
:alt: PyPI version info
.. image:: https://img.shields.io/pypi/pyversions/discord.py.svg
:target: https://pypi.python.org/pypi/discord.py
.. image:: https://img.shields.io/pypi/pyversions/enhanced-dpy.svg
:target: https://pypi.python.org/pypi/enhanced-dpy
:alt: PyPI supported Python versions
A modern, easy to use, feature-rich, and async ready API wrapper for Discord written in Python.
A modern, maintained, easy to use, feature-rich, and async ready API wrapper for Discord written in Python.
The Future of enhanced-discord.py
--------------------------
Enhanced discord.py is a fork of Rapptz's discord.py, that went unmaintained (`gist <https://gist.github.com/Rapptz/4a2f62751b9600a31a0d3c78100287f1>`_)
An overview of added features is available on the `custom features page <https://enhanced-dpy.readthedocs.io/en/latest/index.html#custom-features>`_.
Key Features
-------------
- Modern Pythonic API using ``async`` and ``await``.
- Proper rate limit handling.
- 100% coverage of the supported Discord API.
- Optimised in both speed and memory.
Installing
----------
**Python 3.5.3 or higher is required**
**Python 3.8 or higher is required**
To install the library without full voice support, you can just run the following command:
.. code:: sh
# Linux/macOS
python3 -m pip install -U discord.py
python3 -m pip install -U enhanced-dpy
# Windows
py -3 -m pip install -U discord.py
Otherwise to get voice support you should run the following command:
.. code:: sh
# Linux/macOS
python3 -m pip install -U "discord.py[voice]"
# Windows
py -3 -m pip install -U discord.py[voice]
py -3 -m pip install -U enhanced-dpy
To install the development version, do the following:
.. code:: sh
$ git clone https://github.com/Rapptz/discord.py
$ cd discord.py
$ git clone https://github.com/iDevision/enhanced-discord.py
$ cd enhanced-discord.py
$ python3 -m pip install -U .[voice]
Optional Packages
~~~~~~~~~~~~~~~~~~
* PyNaCl (for voice support)
* `PyNaCl <https://pypi.org/project/PyNaCl/>`__ (for voice support)
Please note that on Linux installing voice you must install the following packages via your favourite package manager (e.g. ``apt``, ``dnf``, etc) before running the above commands:
@ -109,6 +104,6 @@ You can find more examples in the examples directory.
Links
------
- `Documentation <https://discordpy.readthedocs.io/en/latest/index.html>`_
- `Official Discord Server <https://discord.gg/r3sSKJJ>`_
- `Documentation <https://enhanced-dpy.readthedocs.io/en/latest/index.html>`_
- `Official Discord Server <https://discord.gg/TvqYBrGXEm>`_
- `Discord API <https://discord.gg/discord-api>`_

View File

@ -9,16 +9,16 @@ A basic wrapper for the Discord API.
"""
__title__ = 'discord'
__author__ = 'Rapptz'
__license__ = 'MIT'
__copyright__ = 'Copyright 2015-present Rapptz'
__version__ = '2.0.0a'
__title__ = "discord"
__author__ = "Rapptz"
__license__ = "MIT"
__copyright__ = "Copyright 2015-present Rapptz"
__version__ = "2.0.0a"
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
from collections import namedtuple
import logging
from typing import NamedTuple, Literal
from .client import *
from .appinfo import *
@ -43,7 +43,7 @@ from .template import *
from .widget import *
from .object import *
from .reaction import *
from . import utils, opus, abc
from . import utils, opus, abc, ui
from .enums import *
from .embeds import *
from .mentions import *
@ -55,10 +55,20 @@ from .audit_logs import *
from .raw_models import *
from .team import *
from .sticker import *
from .stage_instance import *
from .interactions import *
from .components import *
from .threads import *
VersionInfo = namedtuple('VersionInfo', 'major minor micro releaselevel serial')
version_info = VersionInfo(major=2, minor=0, micro=0, releaselevel='alpha', serial=0)
class VersionInfo(NamedTuple):
major: int
minor: int
micro: int
releaselevel: Literal["alpha", "beta", "candidate", "final"]
serial: int
version_info: VersionInfo = VersionInfo(major=2, minor=0, micro=0, releaselevel="alpha", serial=0)
logging.getLogger(__name__).addHandler(logging.NullHandler())

View File

@ -31,27 +31,30 @@ import pkg_resources
import aiohttp
import platform
def show_version():
entries = []
entries.append('- Python v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}'.format(sys.version_info))
entries.append("- Python v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}".format(sys.version_info))
version_info = discord.version_info
entries.append('- discord.py v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}'.format(version_info))
if version_info.releaselevel != 'final':
pkg = pkg_resources.get_distribution('discord.py')
entries.append("- discord.py v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}".format(version_info))
if version_info.releaselevel != "final":
pkg = pkg_resources.get_distribution("discord.py")
if pkg:
entries.append(f' - discord.py pkg_resources: v{pkg.version}')
entries.append(f" - discord.py pkg_resources: v{pkg.version}")
entries.append(f'- aiohttp v{aiohttp.__version__}')
entries.append(f"- aiohttp v{aiohttp.__version__}")
uname = platform.uname()
entries.append('- system info: {0.system} {0.release} {0.version}'.format(uname))
print('\n'.join(entries))
entries.append("- system info: {0.system} {0.release} {0.version}".format(uname))
print("\n".join(entries))
def core(parser, args):
if args.version:
show_version()
bot_template = """#!/usr/bin/env python3
_bot_template = """#!/usr/bin/env python3
from discord.ext import commands
import discord
@ -64,10 +67,10 @@ class Bot(commands.{base}):
try:
self.load_extension(cog)
except Exception as exc:
print('Could not load extension {{0}} due to {{1.__class__.__name__}}: {{1}}'.format(cog, exc))
print(f'Could not load extension {{cog}} due to {{exc.__class__.__name__}}: {{exc}}')
async def on_ready(self):
print('Logged on as {{0}} (ID: {{0.id}})'.format(self.user))
print(f'Logged on as {{self.user}} (ID: {{self.user.id}})')
bot = Bot()
@ -77,7 +80,7 @@ bot = Bot()
bot.run(config.token)
"""
gitignore_template = """# Byte-compiled / optimized / DLL files
_gitignore_template = """# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
@ -107,7 +110,7 @@ var/
config.py
"""
cog_template = '''from discord.ext import commands
_cog_template = '''from discord.ext import commands
import discord
class {name}(commands.Cog{attrs}):
@ -120,7 +123,7 @@ def setup(bot):
bot.add_cog({name}(bot))
'''
cog_extras = '''
_cog_extras = """
def cog_unload(self):
# clean up logic goes here
pass
@ -149,44 +152,68 @@ cog_extras = '''
# 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
# '\\': '-',
'|': '-',
'?': '-',
'*': '-',
"|": "-",
"?": "-",
"*": "-",
}
# NUL (0) and 1-31 are disallowed
_base_table.update((chr(i), None) for i in range(32))
translation_table = str.maketrans(_base_table)
_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 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')
parser.error("invalid directory name given, use a different one")
name = name.translate(translation_table)
name = name.translate(_translation_table)
if replace_spaces:
name = name.replace(' ', '-')
name = name.replace(" ", "-")
return Path(name)
def newbot(parser, args):
new_directory = to_path(parser, args.directory) / to_path(parser, args.name)
@ -195,106 +222,114 @@ def newbot(parser, args):
try:
new_directory.mkdir(exist_ok=True, parents=True)
except OSError as exc:
parser.error(f'could not create our bot directory ({exc})')
parser.error(f"could not create our bot directory ({exc})")
cogs = new_directory / 'cogs'
cogs = new_directory / "cogs"
try:
cogs.mkdir(exist_ok=True)
init = cogs / '__init__.py'
init = cogs / "__init__.py"
init.touch()
except OSError as exc:
print(f'warning: could not create cogs directory ({exc})')
print(f"warning: could not create cogs directory ({exc})")
try:
with open(str(new_directory / 'config.py'), 'w', encoding='utf-8') as fp:
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(f'could not create config file ({exc})')
parser.error(f"could not create config file ({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))
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(f'could not create bot file ({exc})')
parser.error(f"could not create bot file ({exc})")
if not args.no_git:
try:
with open(str(new_directory / '.gitignore'), 'w', encoding='utf-8') as fp:
fp.write(gitignore_template)
with open(str(new_directory / ".gitignore"), "w", encoding="utf-8") as fp:
fp.write(_gitignore_template)
except OSError as exc:
print(f'warning: could not create .gitignore file ({exc})')
print(f"warning: could not create .gitignore file ({exc})")
print("successfully made bot at", new_directory)
print('successfully made bot at', new_directory)
def newcog(parser, args):
cog_dir = to_path(parser, args.directory)
try:
cog_dir.mkdir(exist_ok=True)
except OSError as exc:
print(f'warning: could not create cogs directory ({exc})')
print(f"warning: could not create cogs directory ({exc})")
directory = cog_dir / to_path(parser, args.name)
directory = directory.with_suffix('.py')
directory = directory.with_suffix(".py")
try:
with open(str(directory), 'w', encoding='utf-8') as fp:
attrs = ''
extra = cog_extras if args.full else ''
with open(str(directory), "w", encoding="utf-8") as fp:
attrs = ""
extra = _cog_extras if args.full else ""
if args.class_name:
name = args.class_name
else:
name = str(directory.stem)
if '-' in name or '_' in name:
translation = str.maketrans('-_', ' ')
name = name.translate(translation).title().replace(' ', '')
if "-" in name or "_" in name:
translation = str.maketrans("-_", " ")
name = name.translate(translation).title().replace(" ", "")
else:
name = name.title()
if args.display_name:
attrs += f', name="{args.display_name}"'
if args.hide_commands:
attrs += ', command_attrs=dict(hidden=True)'
fp.write(cog_template.format(name=name, extra=extra, attrs=attrs))
attrs += ", command_attrs=dict(hidden=True)"
fp.write(_cog_template.format(name=name, extra=extra, attrs=attrs))
except OSError as exc:
parser.error(f'could not create cog file ({exc})')
parser.error(f"could not create cog file ({exc})")
else:
print('successfully made cog at', directory)
print("successfully made cog at", directory)
def add_newbot_args(subparser):
parser = subparser.add_parser('newbot', help='creates a command bot project quickly')
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')
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 = 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('--display-name', help='the cog name (default: <name>)')
parser.add_argument('--hide-commands', help='whether to hide all commands in the cog', action='store_true')
parser.add_argument('--full', help='add all special methods as well', action='store_true')
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("--display-name", help="the cog name (default: <name>)")
parser.add_argument("--hide-commands", help="whether to hide all commands in the cog", action="store_true")
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')
parser.add_argument('-v', '--version', action='store_true', help='shows the library version')
parser = argparse.ArgumentParser(prog="discord", description="Tools for helping with discord.py")
parser.add_argument("-v", "--version", action="store_true", help="shows the library version")
parser.set_defaults(func=core)
subparser = parser.add_subparsers(dest='subcommand', title='subcommands')
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)
if __name__ == '__main__':
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import datetime
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union, overload
from .asset import Asset
from .enums import ActivityType, try_enum
@ -31,12 +34,12 @@ from .partial_emoji import PartialEmoji
from .utils import _get_as_snowflake
__all__ = (
'BaseActivity',
'Activity',
'Streaming',
'Game',
'Spotify',
'CustomActivity',
"BaseActivity",
"Activity",
"Streaming",
"Game",
"Spotify",
"CustomActivity",
)
"""If curious, this is the current schema for an activity.
@ -71,6 +74,9 @@ type: int
sync_id: str
session_id: str
flags: int
buttons: list[dict]
label: str (max: 32)
url: str (max: 512)
There are also activity flags which are mostly uninteresting for the library atm.
@ -84,6 +90,16 @@ t.ActivityFlags = {
}
"""
if TYPE_CHECKING:
from .types.activity import (
Activity as ActivityPayload,
ActivityTimestamps,
ActivityParty,
ActivityAssets,
ActivityButton,
)
class BaseActivity:
"""The base activity that all user-settable activities inherit from.
A user-settable activity is one that can be used in :meth:`Client.change_presence`.
@ -102,19 +118,24 @@ class BaseActivity:
.. versionadded:: 1.3
"""
__slots__ = ('_created_at',)
__slots__ = ("_created_at",)
def __init__(self, **kwargs):
self._created_at = kwargs.pop('created_at', None)
self._created_at: Optional[float] = kwargs.pop("created_at", None)
@property
def created_at(self):
def created_at(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC.
.. versionadded:: 1.3
"""
if self._created_at is not None:
return datetime.datetime.utcfromtimestamp(self._created_at / 1000)
return datetime.datetime.fromtimestamp(self._created_at / 1000, tz=datetime.timezone.utc)
def to_dict(self) -> ActivityPayload:
raise NotImplementedError
class Activity(BaseActivity):
"""Represents an activity in Discord.
@ -130,17 +151,17 @@ class Activity(BaseActivity):
Attributes
------------
application_id: :class:`int`
application_id: Optional[:class:`int`]
The application ID of the game.
name: :class:`str`
name: Optional[:class:`str`]
The name of the activity.
url: :class:`str`
url: Optional[:class:`str`]
A stream URL that the activity could be doing.
type: :class:`ActivityType`
The type of activity currently being done.
state: :class:`str`
state: Optional[:class:`str`]
The user's current state. For example, "In Game".
details: :class:`str`
details: Optional[:class:`str`]
The detail of the user's current activity.
timestamps: :class:`dict`
A dictionary of timestamps. It contains the following optional keys:
@ -164,52 +185,75 @@ class Activity(BaseActivity):
- ``id``: A string representing the party ID.
- ``size``: A list of up to two integer elements denoting (current_size, maximum_size).
buttons: List[:class:`dict`]
An list of dictionaries representing custom buttons shown in a rich presence.
Each dictionary contains the following keys:
- ``label``: A string representing the text shown on the button.
- ``url``: A string representing the URL opened upon clicking the button.
.. versionadded:: 2.0
emoji: Optional[:class:`PartialEmoji`]
The emoji that belongs to this activity.
"""
__slots__ = ('state', 'details', '_created_at', 'timestamps', 'assets', 'party',
'flags', 'sync_id', 'session_id', 'type', 'name', 'url',
'application_id', 'emoji')
__slots__ = (
"state",
"details",
"_created_at",
"timestamps",
"assets",
"party",
"flags",
"sync_id",
"session_id",
"type",
"name",
"url",
"application_id",
"emoji",
"buttons",
)
def __init__(self, **kwargs):
super().__init__(**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 = _get_as_snowflake(kwargs, 'application_id')
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.state: Optional[str] = kwargs.pop("state", None)
self.details: Optional[str] = kwargs.pop("details", None)
self.timestamps: ActivityTimestamps = kwargs.pop("timestamps", {})
self.assets: ActivityAssets = kwargs.pop("assets", {})
self.party: ActivityParty = kwargs.pop("party", {})
self.application_id: Optional[int] = _get_as_snowflake(kwargs, "application_id")
self.name: Optional[str] = kwargs.pop("name", None)
self.url: Optional[str] = kwargs.pop("url", None)
self.flags: int = kwargs.pop("flags", 0)
self.sync_id: Optional[str] = kwargs.pop("sync_id", None)
self.session_id: Optional[str] = kwargs.pop("session_id", None)
self.buttons: List[ActivityButton] = kwargs.pop("buttons", [])
activity_type = kwargs.pop('type', -1)
self.type = activity_type if isinstance(activity_type, ActivityType) else try_enum(ActivityType, activity_type)
emoji = kwargs.pop('emoji', None)
if emoji is not None:
self.emoji = PartialEmoji.from_dict(emoji)
else:
self.emoji = None
def __repr__(self):
attrs = (
('type', self.type),
('name', self.name),
('url', self.url),
('details', self.details),
('application_id', self.application_id),
('session_id', self.session_id),
('emoji', self.emoji),
activity_type = kwargs.pop("type", -1)
self.type: ActivityType = (
activity_type if isinstance(activity_type, ActivityType) else try_enum(ActivityType, activity_type)
)
inner = ' '.join('%s=%r' % t for t in attrs)
return f'<Activity {inner}>'
def to_dict(self):
ret = {}
emoji = kwargs.pop("emoji", None)
self.emoji: Optional[PartialEmoji] = PartialEmoji.from_dict(emoji) if emoji is not None else None
def __repr__(self) -> str:
attrs = (
("type", self.type),
("name", self.name),
("url", self.url),
("details", self.details),
("application_id", self.application_id),
("session_id", self.session_id),
("emoji", self.emoji),
)
inner = " ".join("%s=%r" % t for t in attrs)
return f"<Activity {inner}>"
def to_dict(self) -> Dict[str, Any]:
ret: Dict[str, Any] = {}
for attr in self.__slots__:
value = getattr(self, attr, None)
if value is None:
@ -219,65 +263,66 @@ class Activity(BaseActivity):
continue
ret[attr] = value
ret['type'] = int(self.type)
ret["type"] = int(self.type)
if self.emoji:
ret['emoji'] = self.emoji.to_dict()
ret["emoji"] = self.emoji.to_dict()
return ret
@property
def start(self):
def start(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable."""
try:
timestamp = self.timestamps['start'] / 1000
timestamp = self.timestamps["start"] / 1000
except KeyError:
return None
else:
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
@property
def end(self):
def end(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable."""
try:
timestamp = self.timestamps['end'] / 1000
timestamp = self.timestamps["end"] / 1000
except KeyError:
return None
else:
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
@property
def large_image_url(self):
def large_image_url(self) -> Optional[str]:
"""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']
large_image = self.assets["large_image"]
except KeyError:
return None
else:
return Asset.BASE + f'/app-assets/{self.application_id}/{large_image}.png'
return Asset.BASE + f"/app-assets/{self.application_id}/{large_image}.png"
@property
def small_image_url(self):
def small_image_url(self) -> Optional[str]:
"""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']
small_image = self.assets["small_image"]
except KeyError:
return None
else:
return Asset.BASE + f'/app-assets/{self.application_id}/{small_image}.png'
@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)
return Asset.BASE + f"/app-assets/{self.application_id}/{small_image}.png"
@property
def small_image_text(self):
def large_image_text(self) -> Optional[str]:
"""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[str]:
"""Optional[:class:`str`]: Returns the small image asset hover text of this activity if applicable."""
return self.assets.get('small_text', None)
return self.assets.get("small_text", None)
class Game(BaseActivity):
@ -314,23 +359,23 @@ class Game(BaseActivity):
The game's name.
"""
__slots__ = ('name', '_end', '_start')
__slots__ = ("name", "_end", "_start")
def __init__(self, name, **extra):
def __init__(self, name: str, **extra):
super().__init__(**extra)
self.name = name
self.name: str = name
try:
timestamps = extra['timestamps']
timestamps: ActivityTimestamps = extra["timestamps"]
except KeyError:
self._start = 0
self._end = 0
else:
self._start = timestamps.get('start', 0)
self._end = timestamps.get('end', 0)
self._start = timestamps.get("start", 0)
self._end = timestamps.get("end", 0)
@property
def type(self):
def type(self) -> ActivityType:
""":class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`.
It always returns :attr:`ActivityType.playing`.
@ -338,48 +383,51 @@ class Game(BaseActivity):
return ActivityType.playing
@property
def start(self):
def start(self) -> Optional[datetime.datetime]:
"""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).replace(tzinfo=datetime.timezone.utc)
return datetime.datetime.fromtimestamp(self._start / 1000, tz=datetime.timezone.utc)
return None
@property
def end(self):
def end(self) -> Optional[datetime.datetime]:
"""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).replace(tzinfo=datetime.timezone.utc)
return datetime.datetime.fromtimestamp(self._end / 1000, tz=datetime.timezone.utc)
return None
def __str__(self):
def __str__(self) -> str:
return str(self.name)
def __repr__(self):
return f'<Game name={self.name!r}>'
def __repr__(self) -> str:
return f"<Game name={self.name!r}>"
def to_dict(self):
timestamps = {}
def to_dict(self) -> Dict[str, Any]:
timestamps: Dict[str, Any] = {}
if self._start:
timestamps['start'] = self._start
timestamps["start"] = self._start
if self._end:
timestamps['end'] = self._end
timestamps["end"] = self._end
# fmt: off
return {
'type': ActivityType.playing.value,
'name': str(self.name),
'timestamps': timestamps
}
# fmt: on
def __eq__(self, other):
def __eq__(self, other: Any) -> bool:
return isinstance(other, Game) and other.name == self.name
def __ne__(self, other):
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __hash__(self):
def __hash__(self) -> int:
return hash(self.name)
class Streaming(BaseActivity):
"""A slimmed down version of :class:`Activity` that represents a Discord streaming status.
@ -405,7 +453,7 @@ class Streaming(BaseActivity):
Attributes
-----------
platform: :class:`str`
platform: Optional[:class:`str`]
Where the user is streaming from (ie. YouTube, Twitch).
.. versionadded:: 1.3
@ -425,30 +473,30 @@ class Streaming(BaseActivity):
A dictionary comprising of similar keys than those in :attr:`Activity.assets`.
"""
__slots__ = ('platform', 'name', 'game', 'url', 'details', 'assets')
__slots__ = ("platform", "name", "game", "url", "details", "assets")
def __init__(self, *, name, url, **extra):
def __init__(self, *, name: Optional[str], url: str, **extra: Any):
super().__init__(**extra)
self.platform = name
self.name = extra.pop('details', name)
self.game = extra.pop('state', None)
self.url = url
self.details = extra.pop('details', self.name) # compatibility
self.assets = extra.pop('assets', {})
self.platform: Optional[str] = name
self.name: Optional[str] = extra.pop("details", name)
self.game: Optional[str] = extra.pop("state", None)
self.url: str = url
self.details: Optional[str] = extra.pop("details", self.name) # compatibility
self.assets: ActivityAssets = extra.pop("assets", {})
@property
def type(self):
def type(self) -> ActivityType:
""":class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`.
It always returns :attr:`ActivityType.streaming`.
"""
return ActivityType.streaming
def __str__(self):
def __str__(self) -> str:
return str(self.name)
def __repr__(self):
return f'<Streaming name={self.name!r}>'
def __repr__(self) -> str:
return f"<Streaming name={self.name!r}>"
@property
def twitch_name(self):
@ -459,32 +507,35 @@ class Streaming(BaseActivity):
"""
try:
name = self.assets['large_image']
name = self.assets["large_image"]
except KeyError:
return None
else:
return name[7:] if name[:7] == 'twitch:' else None
return name[7:] if name[:7] == "twitch:" else None
def to_dict(self):
ret = {
def to_dict(self) -> Dict[str, Any]:
# fmt: off
ret: Dict[str, Any] = {
'type': ActivityType.streaming.value,
'name': str(self.name),
'url': str(self.url),
'assets': self.assets
}
# fmt: on
if self.details:
ret['details'] = self.details
ret["details"] = self.details
return ret
def __eq__(self, other):
def __eq__(self, other: Any) -> bool:
return isinstance(other, Streaming) and other.name == self.name and other.url == self.url
def __ne__(self, other):
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __hash__(self):
def __hash__(self) -> int:
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.
@ -508,21 +559,20 @@ class Spotify:
Returns the string 'Spotify'.
"""
__slots__ = ('_state', '_details', '_timestamps', '_assets', '_party', '_sync_id', '_session_id',
'_created_at')
__slots__ = ("_state", "_details", "_timestamps", "_assets", "_party", "_sync_id", "_session_id", "_created_at")
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')
self._created_at = data.pop('created_at', None)
self._state: str = data.pop("state", "")
self._details: str = data.pop("details", "")
self._timestamps: Dict[str, int] = data.pop("timestamps", {})
self._assets: ActivityAssets = data.pop("assets", {})
self._party: ActivityParty = data.pop("party", {})
self._sync_id: str = data.pop("sync_id")
self._session_id: str = data.pop("session_id")
self._created_at: Optional[float] = data.pop("created_at", None)
@property
def type(self):
def type(self) -> ActivityType:
""":class:`ActivityType`: Returns the activity's type. This is for compatibility with :class:`Activity`.
It always returns :attr:`ActivityType.listening`.
@ -530,74 +580,78 @@ class Spotify:
return ActivityType.listening
@property
def created_at(self):
def created_at(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: When the user started listening in UTC.
.. versionadded:: 1.3
"""
if self._created_at is not None:
return datetime.datetime.utcfromtimestamp(self._created_at / 1000)
return datetime.datetime.fromtimestamp(self._created_at / 1000, tz=datetime.timezone.utc)
@property
def colour(self):
def colour(self) -> Colour:
""":class:`Colour`: Returns the Spotify integration colour, as a :class:`Colour`.
There is an alias for this named :attr:`color`"""
return Colour(0x1db954)
return Colour(0x1DB954)
@property
def color(self):
def color(self) -> Colour:
""":class:`Colour`: Returns the Spotify integration colour, as a :class:`Colour`.
There is an alias for this named :attr:`colour`"""
return self.colour
def to_dict(self):
def to_dict(self) -> Dict[str, Any]:
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
"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):
def name(self) -> str:
""":class:`str`: The activity's name. This will always return "Spotify"."""
return 'Spotify'
return "Spotify"
def __eq__(self, other):
return (isinstance(other, Spotify) and other._session_id == self._session_id
and other._sync_id == self._sync_id and other.start == self.start)
def __eq__(self, other: Any) -> bool:
return (
isinstance(other, Spotify)
and other._session_id == self._session_id
and other._sync_id == self._sync_id
and other.start == self.start
)
def __ne__(self, other):
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __hash__(self):
def __hash__(self) -> int:
return hash(self._session_id)
def __str__(self):
return 'Spotify'
def __str__(self) -> str:
return "Spotify"
def __repr__(self):
return '<Spotify title={0.title!r} artist={0.artist!r} track_id={0.track_id!r}>'.format(self)
def __repr__(self) -> str:
return f"<Spotify title={self.title!r} artist={self.artist!r} track_id={self.track_id!r}>"
@property
def title(self):
def title(self) -> str:
""":class:`str`: The title of the song being played."""
return self._details
@property
def artists(self):
def artists(self) -> List[str]:
"""List[:class:`str`]: The artists of the song being played."""
return self._state.split('; ')
return self._state.split("; ")
@property
def artist(self):
def artist(self) -> str:
""":class:`str`: The artist of the song being played.
This does not attempt to split the artist information into
@ -606,43 +660,52 @@ class Spotify:
return self._state
@property
def album(self):
def album(self) -> str:
""":class:`str`: The album that the song being played belongs to."""
return self._assets.get('large_text', '')
return self._assets.get("large_text", "")
@property
def album_cover_url(self):
def album_cover_url(self) -> str:
""":class:`str`: The album cover image URL from Spotify's CDN."""
large_image = self._assets.get('large_image', '')
if large_image[:8] != 'spotify:':
return ''
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
return "https://i.scdn.co/image/" + album_image_id
@property
def track_id(self):
def track_id(self) -> str:
""":class:`str`: The track ID used by Spotify to identify this song."""
return self._sync_id
@property
def start(self):
def track_url(self) -> str:
""":class:`str`: The track URL to listen on Spotify.
.. versionadded:: 2.0
"""
return f"https://open.spotify.com/track/{self.track_id}"
@property
def start(self) -> datetime.datetime:
""":class:`datetime.datetime`: When the user started playing this song in UTC."""
return datetime.datetime.utcfromtimestamp(self._timestamps['start'] / 1000)
return datetime.datetime.fromtimestamp(self._timestamps["start"] / 1000, tz=datetime.timezone.utc)
@property
def end(self):
def end(self) -> datetime.datetime:
""":class:`datetime.datetime`: When the user will stop playing this song in UTC."""
return datetime.datetime.utcfromtimestamp(self._timestamps['end'] / 1000)
return datetime.datetime.fromtimestamp(self._timestamps["end"] / 1000, tz=datetime.timezone.utc)
@property
def duration(self):
def duration(self) -> datetime.timedelta:
""":class:`datetime.timedelta`: The duration of the song being played."""
return self.end - self.start
@property
def party_id(self):
def party_id(self) -> str:
""":class:`str`: The party ID of the listening party."""
return self._party.get('id', '')
return self._party.get("id", "")
class CustomActivity(BaseActivity):
"""Represents a Custom activity from Discord.
@ -675,15 +738,16 @@ class CustomActivity(BaseActivity):
The emoji to pass to the activity, if any.
"""
__slots__ = ('name', 'emoji', 'state')
__slots__ = ("name", "emoji", "state")
def __init__(self, name, *, emoji=None, **extra):
def __init__(self, name: Optional[str], *, emoji: Optional[PartialEmoji] = None, **extra: Any):
super().__init__(**extra)
self.name = name
self.state = extra.pop('state', None)
if self.name == 'Custom Status':
self.name: Optional[str] = name
self.state: Optional[str] = extra.pop("state", None)
if self.name == "Custom Status":
self.name = self.state
self.emoji: Optional[PartialEmoji]
if emoji is None:
self.emoji = emoji
elif isinstance(emoji, dict):
@ -693,74 +757,89 @@ class CustomActivity(BaseActivity):
elif isinstance(emoji, PartialEmoji):
self.emoji = emoji
else:
raise TypeError(f'Expected str, PartialEmoji, or None, received {type(emoji)!r} instead.')
raise TypeError(f"Expected str, PartialEmoji, or None, received {type(emoji)!r} instead.")
@property
def type(self):
def type(self) -> ActivityType:
""":class:`ActivityType`: Returns the activity's type. This is for compatibility with :class:`Activity`.
It always returns :attr:`ActivityType.custom`.
"""
return ActivityType.custom
def to_dict(self):
def to_dict(self) -> Dict[str, Any]:
if self.name == self.state:
o = {
'type': ActivityType.custom.value,
'state': self.name,
'name': 'Custom Status',
"type": ActivityType.custom.value,
"state": self.name,
"name": "Custom Status",
}
else:
o = {
'type': ActivityType.custom.value,
'name': self.name,
"type": ActivityType.custom.value,
"name": self.name,
}
if self.emoji:
o['emoji'] = self.emoji.to_dict()
o["emoji"] = self.emoji.to_dict()
return o
def __eq__(self, other):
return (isinstance(other, CustomActivity) and other.name == self.name and other.emoji == self.emoji)
def __eq__(self, other: Any) -> bool:
return isinstance(other, CustomActivity) and other.name == self.name and other.emoji == self.emoji
def __ne__(self, other):
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __hash__(self):
def __hash__(self) -> int:
return hash((self.name, str(self.emoji)))
def __str__(self):
def __str__(self) -> str:
if self.emoji:
if self.name:
return f'{self.emoji} {self.name}'
return f"{self.emoji} {self.name}"
return str(self.emoji)
else:
return str(self.name)
def __repr__(self):
return '<CustomActivity name={0.name!r} emoji={0.emoji!r}>'.format(self)
def __repr__(self) -> str:
return f"<CustomActivity name={self.name!r} emoji={self.emoji!r}>"
def create_activity(data):
ActivityTypes = Union[Activity, Game, CustomActivity, Streaming, Spotify]
@overload
def create_activity(data: ActivityPayload) -> ActivityTypes:
...
@overload
def create_activity(data: None) -> None:
...
def create_activity(data: Optional[ActivityPayload]) -> Optional[ActivityTypes]:
if not data:
return None
game_type = try_enum(ActivityType, data.get('type', -1))
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:
if "application_id" in data or "session_id" in data:
return Activity(**data)
return Game(**data)
elif game_type is ActivityType.custom:
try:
name = data.pop('name')
name = data.pop("name")
except KeyError:
return Activity(**data)
else:
return CustomActivity(name=name, **data)
# we removed the name key from data already
return CustomActivity(name=name, **data) # type: ignore
elif game_type is ActivityType.streaming:
if 'url' in data:
return Streaming(**data)
if "url" in data:
# the url won't be None here
return Streaming(**data) # type: ignore
return Activity(**data)
elif game_type is ActivityType.listening and 'sync_id' in data and 'session_id' in data:
elif game_type is ActivityType.listening and "sync_id" in data and "session_id" in data:
return Spotify(**data)
return Activity(**data)

View File

@ -22,15 +22,29 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import List, TYPE_CHECKING, Optional
from . import utils
from .user import User
from .asset import Asset
from .team import Team
if TYPE_CHECKING:
from .guild import Guild
from .types.appinfo import (
AppInfo as AppInfoPayload,
PartialAppInfo as PartialAppInfoPayload,
Team as TeamPayload,
)
from .user import User
from .state import ConnectionState
__all__ = (
'AppInfo',
"AppInfo",
"PartialAppInfo",
)
class AppInfo:
"""Represents the application info for the bot provided by Discord.
@ -48,9 +62,7 @@ class AppInfo:
.. versionadded:: 1.3
icon: Optional[:class:`str`]
The icon hash, if it exists.
description: Optional[:class:`str`]
description: :class:`str`
The application description.
bot_public: :class:`bool`
Whether the bot can be invited by anyone or if it is locked
@ -62,155 +74,186 @@ class AppInfo:
A list of RPC origin URLs, if RPC is enabled.
summary: :class:`str`
If this application is a game sold on Discord,
this field will be the summary field for the store page of its primary SKU
this field will be the summary field for the store page of its primary SKU.
.. versionadded:: 1.3
verify_key: :class:`str`
The base64 encoded key for the GameSDK's GetTicket
The hex encoded key for verification in interactions and the
GameSDK's `GetTicket <https://discord.com/developers/docs/game-sdk/applications#getticket>`_.
.. versionadded:: 1.3
guild_id: Optional[:class:`int`]
If this application is a game sold on Discord,
this field will be the guild to which it has been linked
this field will be the guild to which it has been linked to.
.. versionadded:: 1.3
primary_sku_id: Optional[:class:`int`]
If this application is a game sold on Discord,
this field will be the id of the "Game SKU" that is created, if exists
this field will be the id of the "Game SKU" that is created,
if it exists.
.. versionadded:: 1.3
slug: Optional[:class:`str`]
If this application is a game sold on Discord,
this field will be the URL slug that links to the store page
this field will be the URL slug that links to the store page.
.. versionadded:: 1.3
cover_image: Optional[:class:`str`]
If this application is a game sold on Discord,
this field will be the hash of the image on store embeds
terms_of_service_url: Optional[:class:`str`]
The application's terms of service URL, if set.
.. versionadded:: 1.3
.. versionadded:: 2.0
privacy_policy_url: Optional[:class:`str`]
The application's privacy policy URL, if set.
.. versionadded:: 2.0
"""
__slots__ = ('_state', 'description', 'id', 'name', 'rpc_origins',
'bot_public', 'bot_require_code_grant', 'owner', 'icon',
'summary', 'verify_key', 'team', 'guild_id', 'primary_sku_id',
'slug', 'cover_image')
def __init__(self, state, data):
self._state = state
__slots__ = (
"_state",
"description",
"id",
"name",
"rpc_origins",
"bot_public",
"bot_require_code_grant",
"owner",
"_icon",
"summary",
"verify_key",
"team",
"guild_id",
"primary_sku_id",
"slug",
"_cover_image",
"terms_of_service_url",
"privacy_policy_url",
)
self.id = int(data['id'])
self.name = data['name']
self.description = data['description']
self.icon = data['icon']
self.rpc_origins = data['rpc_origins']
self.bot_public = data['bot_public']
self.bot_require_code_grant = data['bot_require_code_grant']
self.owner = User(state=self._state, data=data['owner'])
def __init__(self, state: ConnectionState, data: AppInfoPayload):
from .team import Team
team = data.get('team')
self.team = Team(state, team) if team else None
self._state: ConnectionState = state
self.id: int = int(data["id"])
self.name: str = data["name"]
self.description: str = data["description"]
self._icon: Optional[str] = data["icon"]
self.rpc_origins: List[str] = data["rpc_origins"]
self.bot_public: bool = data["bot_public"]
self.bot_require_code_grant: bool = data["bot_require_code_grant"]
self.owner: User = state.create_user(data["owner"])
self.summary = data['summary']
self.verify_key = data['verify_key']
team: Optional[TeamPayload] = data.get("team")
self.team: Optional[Team] = Team(state, team) if team else None
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
self.summary: str = data["summary"]
self.verify_key: str = data["verify_key"]
self.primary_sku_id = utils._get_as_snowflake(data, 'primary_sku_id')
self.slug = data.get('slug')
self.cover_image = data.get('cover_image')
self.guild_id: Optional[int] = utils._get_as_snowflake(data, "guild_id")
def __repr__(self):
return '<{0.__class__.__name__} id={0.id} name={0.name!r} description={0.description!r} public={0.bot_public} ' \
'owner={0.owner!r}>'.format(self)
self.primary_sku_id: Optional[int] = utils._get_as_snowflake(data, "primary_sku_id")
self.slug: Optional[str] = data.get("slug")
self._cover_image: Optional[str] = data.get("cover_image")
self.terms_of_service_url: Optional[str] = data.get("terms_of_service_url")
self.privacy_policy_url: Optional[str] = data.get("privacy_policy_url")
def __repr__(self) -> str:
return (
f"<{self.__class__.__name__} id={self.id} name={self.name!r} "
f"description={self.description!r} public={self.bot_public} "
f"owner={self.owner!r}>"
)
@property
def icon_url(self):
""":class:`.Asset`: Retrieves the application's icon asset.
This is equivalent to calling :meth:`icon_url_as` with
the default parameters ('webp' format and a size of 1024).
.. versionadded:: 1.3
"""
return self.icon_url_as()
def icon_url_as(self, *, format='webp', size=1024):
"""Returns an :class:`Asset` for the icon the application has.
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
The size must be a power of 2 between 16 and 4096.
.. versionadded:: 1.6
Parameters
-----------
format: :class:`str`
The format to attempt to convert the icon to. Defaults to 'webp'.
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_icon(self._state, self, 'app', format=format, size=size)
def icon(self) -> Optional[Asset]:
"""Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any."""
if self._icon is None:
return None
return Asset._from_icon(self._state, self.id, self._icon, path="app")
@property
def cover_image_url(self):
""":class:`.Asset`: Retrieves the cover image on a store embed.
def cover_image(self) -> Optional[Asset]:
"""Optional[:class:`.Asset`]: Retrieves the cover image on a store embed, if any.
This is equivalent to calling :meth:`cover_image_url_as` with
the default parameters ('webp' format and a size of 1024).
.. versionadded:: 1.3
This is only available if the application is a game sold on Discord.
"""
return self.cover_image_url_as()
def cover_image_url_as(self, *, format='webp', size=1024):
"""Returns an :class:`Asset` for the image on store embeds
if this application is a game sold on Discord.
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
The size must be a power of 2 between 16 and 4096.
.. versionadded:: 1.6
Parameters
-----------
format: :class:`str`
The format to attempt to convert the image to. Defaults to 'webp'.
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_cover_image(self._state, self, format=format, size=size)
if self._cover_image is None:
return None
return Asset._from_cover_image(self._state, self.id, self._cover_image)
@property
def guild(self):
def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: If this application is a game sold on Discord,
this field will be the guild to which it has been linked
.. versionadded:: 1.3
"""
return self._state._get_guild(int(self.guild_id))
return self._state._get_guild(self.guild_id)
class PartialAppInfo:
"""Represents a partial AppInfo given by :func:`~discord.abc.GuildChannel.create_invite`
.. versionadded:: 2.0
Attributes
-------------
id: :class:`int`
The application ID.
name: :class:`str`
The application name.
description: :class:`str`
The application description.
rpc_origins: Optional[List[:class:`str`]]
A list of RPC origin URLs, if RPC is enabled.
summary: :class:`str`
If this application is a game sold on Discord,
this field will be the summary field for the store page of its primary SKU.
verify_key: :class:`str`
The hex encoded key for verification in interactions and the
GameSDK's `GetTicket <https://discord.com/developers/docs/game-sdk/applications#getticket>`_.
terms_of_service_url: Optional[:class:`str`]
The application's terms of service URL, if set.
privacy_policy_url: Optional[:class:`str`]
The application's privacy policy URL, if set.
"""
__slots__ = (
"_state",
"id",
"name",
"description",
"rpc_origins",
"summary",
"verify_key",
"terms_of_service_url",
"privacy_policy_url",
"_icon",
)
def __init__(self, *, state: ConnectionState, data: PartialAppInfoPayload):
self._state: ConnectionState = state
self.id: int = int(data["id"])
self.name: str = data["name"]
self._icon: Optional[str] = data.get("icon")
self.description: str = data["description"]
self.rpc_origins: Optional[List[str]] = data.get("rpc_origins")
self.summary: str = data["summary"]
self.verify_key: str = data["verify_key"]
self.terms_of_service_url: Optional[str] = data.get("terms_of_service_url")
self.privacy_policy_url: Optional[str] = data.get("privacy_policy_url")
def __repr__(self) -> str:
return f"<{self.__class__.__name__} id={self.id} name={self.name!r} description={self.description!r}>"
@property
def icon(self) -> Optional[Asset]:
"""Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any."""
if self._icon is None:
return None
return Asset._from_icon(self._state, self.id, self._icon, path="app")

View File

@ -22,19 +22,100 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import io
import os
from typing import Any, Literal, Optional, TYPE_CHECKING, Tuple, Union
from .errors import DiscordException
from .errors import InvalidArgument
from . import utils
__all__ = (
'Asset',
)
import yarl
__all__ = ("Asset",)
if TYPE_CHECKING:
ValidStaticFormatTypes = Literal["webp", "jpeg", "jpg", "png"]
ValidAssetFormatTypes = Literal["webp", "jpeg", "jpg", "png", "gif"]
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"}
VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {"gif"}
class Asset:
MISSING = utils.MISSING
class AssetMixin:
url: str
_state: Optional[Any]
async def read(self) -> bytes:
"""|coro|
Retrieves the content of this asset as a :class:`bytes` object.
Raises
------
DiscordException
There was no internal connection state.
HTTPException
Downloading the asset failed.
NotFound
The asset was deleted.
Returns
-------
:class:`bytes`
The content of the asset.
"""
if self._state is None:
raise DiscordException("Invalid state (no ConnectionState provided)")
return await self._state.http.get_from_cdn(self.url)
async def save(self, fp: Union[str, bytes, os.PathLike, io.BufferedIOBase], *, seek_begin: bool = True) -> int:
"""|coro|
Saves this asset into a file-like object.
Parameters
----------
fp: Union[:class:`io.BufferedIOBase`, :class:`os.PathLike`]
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: :class:`bool`
Whether to seek to the beginning of the file after saving is
successfully done.
Raises
------
DiscordException
There was no internal connection state.
HTTPException
Downloading the asset failed.
NotFound
The asset was deleted.
Returns
--------
:class:`int`
The number of bytes written.
"""
data = await self.read()
if isinstance(fp, io.BufferedIOBase):
written = fp.write(data)
if seek_begin:
fp.seek(0)
return written
else:
with open(fp, "wb") as f:
return f.write(data)
class Asset(AssetMixin):
"""Represents a CDN asset on Discord.
.. container:: operations
@ -47,10 +128,6 @@ class Asset:
Returns the length of the CDN asset's URL.
.. describe:: bool(x)
Checks if the Asset has a URL.
.. describe:: x == y
Checks if the asset is equal to another asset.
@ -63,202 +140,276 @@ class Asset:
Returns the hash of the asset.
"""
__slots__ = ('_state', '_url')
BASE = 'https://cdn.discordapp.com'
__slots__: Tuple[str, ...] = (
"_state",
"_url",
"_animated",
"_key",
)
def __init__(self, state, url=None):
BASE = "https://cdn.discordapp.com"
def __init__(self, state, *, url: str, key: str, animated: bool = False):
self._state = state
self._url = url
self._animated = animated
self._key = key
@classmethod
def _from_avatar(cls, state, user, *, format=None, static_format='webp', size=1024):
if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
if format is not None and format not in VALID_AVATAR_FORMATS:
raise InvalidArgument(f"format must be None or one of {VALID_AVATAR_FORMATS}")
if format == "gif" and not user.is_avatar_animated():
raise InvalidArgument("non animated avatars do not support gif format")
if static_format not in VALID_STATIC_FORMATS:
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
if user.avatar is None:
return user.default_avatar_url
if format is None:
format = 'gif' if user.is_avatar_animated() else static_format
return cls(state, '/avatars/{0.id}/{0.avatar}.{1}?size={2}'.format(user, format, size))
def _from_default_avatar(cls, state, index: int) -> Asset:
return cls(
state,
url=f"{cls.BASE}/embed/avatars/{index}.png",
key=str(index),
animated=False,
)
@classmethod
def _from_icon(cls, state, object, path, *, format='webp', size=1024):
if object.icon is None:
return cls(state)
if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
if format not in VALID_STATIC_FORMATS:
raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}")
url = '/{0}-icons/{1.id}/{1.icon}.{2}?size={3}'.format(path, object, format, size)
return cls(state, url)
def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset:
animated = avatar.startswith("a_")
format = "gif" if animated else "png"
return cls(
state,
url=f"{cls.BASE}/avatars/{user_id}/{avatar}.{format}?size=1024",
key=avatar,
animated=animated,
)
@classmethod
def _from_cover_image(cls, state, obj, *, format='webp', size=1024):
if obj.cover_image is None:
return cls(state)
if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
if format not in VALID_STATIC_FORMATS:
raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}")
url = '/app-assets/{0.id}/store/{0.cover_image}.{1}?size={2}'.format(obj, format, size)
return cls(state, url)
def _from_guild_avatar(cls, state, guild_id: int, member_id: int, avatar: str) -> Asset:
animated = avatar.startswith("a_")
format = "gif" if animated else "png"
return cls(
state,
url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.{format}?size=1024",
key=avatar,
animated=animated,
)
@classmethod
def _from_guild_image(cls, state, id, hash, key, *, format='webp', size=1024):
if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
if format not in VALID_STATIC_FORMATS:
raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}")
if hash is None:
return cls(state)
url = '/{key}/{0}/{1}.{2}?size={3}'
return cls(state, url.format(id, hash, format, size, key=key))
def _from_icon(cls, state, object_id: int, icon_hash: str, path: str) -> Asset:
return cls(
state,
url=f"{cls.BASE}/{path}-icons/{object_id}/{icon_hash}.png?size=1024",
key=icon_hash,
animated=False,
)
@classmethod
def _from_guild_icon(cls, state, guild, *, format=None, static_format='webp', size=1024):
if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
if format is not None and format not in VALID_AVATAR_FORMATS:
raise InvalidArgument(f"format must be one of {VALID_AVATAR_FORMATS}")
if format == "gif" and not guild.is_icon_animated():
raise InvalidArgument("non animated guild icons do not support gif format")
if static_format not in VALID_STATIC_FORMATS:
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
if guild.icon is None:
return cls(state)
if format is None:
format = 'gif' if guild.is_icon_animated() else static_format
return cls(state, '/icons/{0.id}/{0.icon}.{1}?size={2}'.format(guild, format, size))
def _from_cover_image(cls, state, object_id: int, cover_image_hash: str) -> Asset:
return cls(
state,
url=f"{cls.BASE}/app-assets/{object_id}/store/{cover_image_hash}.png?size=1024",
key=cover_image_hash,
animated=False,
)
@classmethod
def _from_sticker_url(cls, state, sticker, *, size=1024):
if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
return cls(state, '/stickers/{0.id}/{0.image}.png?size={2}'.format(sticker, format, size))
def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset:
return cls(
state,
url=f"{cls.BASE}/{path}/{guild_id}/{image}.png?size=1024",
key=image,
animated=False,
)
@classmethod
def _from_emoji(cls, state, emoji, *, format=None, static_format='png'):
if format is not None and format not in VALID_AVATAR_FORMATS:
raise InvalidArgument(f"format must be None or one of {VALID_AVATAR_FORMATS}")
if format == "gif" and not emoji.animated:
raise InvalidArgument("non animated emoji's do not support gif format")
if static_format not in VALID_STATIC_FORMATS:
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
if format is None:
format = 'gif' if emoji.animated else static_format
def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset:
animated = icon_hash.startswith("a_")
format = "gif" if animated else "png"
return cls(
state,
url=f"{cls.BASE}/icons/{guild_id}/{icon_hash}.{format}?size=1024",
key=icon_hash,
animated=animated,
)
return cls(state, f'/emojis/{emoji.id}.{format}')
@classmethod
def _from_sticker_banner(cls, state, banner: int) -> Asset:
return cls(
state,
url=f"{cls.BASE}/app-assets/710982414301790216/store/{banner}.png",
key=str(banner),
animated=False,
)
def __str__(self):
return self.BASE + self._url if self._url is not None else ''
@classmethod
def _from_user_banner(cls, state, user_id: int, banner_hash: str) -> Asset:
animated = banner_hash.startswith("a_")
format = "gif" if animated else "png"
return cls(
state,
url=f"{cls.BASE}/banners/{user_id}/{banner_hash}.{format}?size=512",
key=banner_hash,
animated=animated,
)
def __len__(self):
if self._url:
return len(self.BASE + self._url)
return 0
def __str__(self) -> str:
return self._url
def __bool__(self):
return self._url is not None
def __len__(self) -> int:
return len(self._url)
def __repr__(self):
return f'<Asset url={self._url!r}>'
shorten = self._url.replace(self.BASE, "")
return f"<Asset url={shorten!r}>"
def __eq__(self, other):
return isinstance(other, Asset) and self._url == other._url
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self._url)
async def read(self):
"""|coro|
@property
def url(self) -> str:
""":class:`str`: Returns the underlying URL of the asset."""
return self._url
Retrieves the content of this asset as a :class:`bytes` object.
@property
def key(self) -> str:
""":class:`str`: Returns the identifying key of the asset."""
return self._key
.. warning::
def is_animated(self) -> bool:
""":class:`bool`: Returns whether the asset is animated."""
return self._animated
:class:`PartialEmoji` won't have a connection state if user created,
and a URL won't be present if a custom image isn't associated with
the asset, e.g. a guild with no custom icon.
.. versionadded:: 1.1
Raises
------
DiscordException
There was no valid URL or internal connection state.
HTTPException
Downloading the asset failed.
NotFound
The asset was deleted.
Returns
-------
:class:`bytes`
The content of the asset.
"""
if not self._url:
raise DiscordException('Invalid asset (no URL provided)')
if self._state is None:
raise DiscordException('Invalid state (no ConnectionState provided)')
return await self._state.http.get_from_cdn(self.BASE + self._url)
async def save(self, fp, *, seek_begin=True):
"""|coro|
Saves this asset into a file-like object.
def replace(
self,
*,
size: int = MISSING,
format: ValidAssetFormatTypes = MISSING,
static_format: ValidStaticFormatTypes = MISSING,
) -> Asset:
"""Returns a new asset with the passed components replaced.
Parameters
----------
fp: Union[BinaryIO, :class:`os.PathLike`]
Same as in :meth:`Attachment.save`.
seek_begin: :class:`bool`
Same as in :meth:`Attachment.save`.
-----------
size: :class:`int`
The new size of the asset.
format: :class:`str`
The new format to change it to. Must be either
'webp', 'jpeg', 'jpg', 'png', or 'gif' if it's animated.
static_format: :class:`str`
The new format to change it to if the asset isn't animated.
Must be either 'webp', 'jpeg', 'jpg', or 'png'.
Raises
------
DiscordException
There was no valid URL or internal connection state.
HTTPException
Downloading the asset failed.
NotFound
The asset was deleted.
-------
InvalidArgument
An invalid size or format was passed.
Returns
--------
:class:`int`
The number of bytes written.
:class:`Asset`
The newly updated asset.
"""
url = yarl.URL(self._url)
path, _ = os.path.splitext(url.path)
if format is not MISSING:
if self._animated:
if format not in VALID_ASSET_FORMATS:
raise InvalidArgument(f"format must be one of {VALID_ASSET_FORMATS}")
url = url.with_path(f"{path}.{format}")
elif static_format is MISSING:
if format not in VALID_STATIC_FORMATS:
raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}")
url = url.with_path(f"{path}.{format}")
if static_format is not MISSING and not self._animated:
if static_format not in VALID_STATIC_FORMATS:
raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}")
url = url.with_path(f"{path}.{static_format}")
if size is not MISSING:
if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
url = url.with_query(size=size)
else:
url = url.with_query(url.raw_query_string)
url = str(url)
return Asset(state=self._state, url=url, key=self._key, animated=self._animated)
def with_size(self, size: int, /) -> Asset:
"""Returns a new asset with the specified size.
Parameters
------------
size: :class:`int`
The new size of the asset.
Raises
-------
InvalidArgument
The asset had an invalid size.
Returns
--------
:class:`Asset`
The new updated asset.
"""
if not utils.valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
url = str(yarl.URL(self._url).with_query(size=size))
return Asset(state=self._state, url=url, key=self._key, animated=self._animated)
def with_format(self, format: ValidAssetFormatTypes, /) -> Asset:
"""Returns a new asset with the specified format.
Parameters
------------
format: :class:`str`
The new format of the asset.
Raises
-------
InvalidArgument
The asset had an invalid format.
Returns
--------
:class:`Asset`
The new updated asset.
"""
data = await self.read()
if isinstance(fp, io.IOBase) and fp.writable():
written = fp.write(data)
if seek_begin:
fp.seek(0)
return written
if self._animated:
if format not in VALID_ASSET_FORMATS:
raise InvalidArgument(f"format must be one of {VALID_ASSET_FORMATS}")
else:
with open(fp, 'wb') as f:
return f.write(data)
if format not in VALID_STATIC_FORMATS:
raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}")
url = yarl.URL(self._url)
path, _ = os.path.splitext(url.path)
url = str(url.with_path(f"{path}.{format}").with_query(url.raw_query_string))
return Asset(state=self._state, url=url, key=self._key, animated=self._animated)
def with_static_format(self, format: ValidStaticFormatTypes, /) -> Asset:
"""Returns a new asset with the specified static format.
This only changes the format if the underlying asset is
not animated. Otherwise, the asset is not changed.
Parameters
------------
format: :class:`str`
The new static format of the asset.
Raises
-------
InvalidArgument
The asset had an invalid format.
Returns
--------
:class:`Asset`
The new updated asset.
"""
if self._animated:
return self
return self.with_format(format)

View File

@ -22,64 +22,91 @@ 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 __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Generator, List, Optional, Tuple, Type, TypeVar, Union
from . import enums, utils
from .asset import Asset
from .colour import Colour
from .invite import Invite
from .mixins import Hashable
from .object import Object
from .permissions import PermissionOverwrite, Permissions
__all__ = (
'AuditLogDiff',
'AuditLogChanges',
'AuditLogEntry',
"AuditLogDiff",
"AuditLogChanges",
"AuditLogEntry",
)
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)
if TYPE_CHECKING:
import datetime
def _transform_explicit_content_filter(entry, data):
return enums.try_enum(enums.ContentFilter, data)
from . import abc
from .emoji import Emoji
from .guild import Guild
from .member import Member
from .role import Role
from .types.audit_log import (
AuditLogChange as AuditLogChangePayload,
AuditLogEntry as AuditLogEntryPayload,
)
from .types.channel import PermissionOverwrite as PermissionOverwritePayload
from .types.role import Role as RolePayload
from .types.snowflake import Snowflake
from .user import User
from .stage_instance import StageInstance
from .sticker import GuildSticker
from .threads import Thread
def _transform_permissions(entry, data):
return Permissions(data)
def _transform_color(entry, data):
def _transform_permissions(entry: AuditLogEntry, data: str) -> Permissions:
return Permissions(int(data))
def _transform_color(entry: AuditLogEntry, data: int) -> Colour:
return Colour(data)
def _transform_snowflake(entry, data):
def _transform_snowflake(entry: AuditLogEntry, data: Snowflake) -> int:
return int(data)
def _transform_channel(entry, data):
def _transform_channel(entry: AuditLogEntry, data: Optional[Snowflake]) -> Optional[Union[abc.GuildChannel, Object]]:
if data is None:
return None
return entry.guild.get_channel(int(data)) or Object(id=data)
def _transform_owner_id(entry, data):
def _transform_member_id(entry: AuditLogEntry, data: Optional[Snowflake]) -> Union[Member, User, None]:
if data is None:
return None
return entry._get_member(int(data))
def _transform_inviter_id(entry, data):
def _transform_guild_id(entry: AuditLogEntry, data: Optional[Snowflake]) -> Optional[Guild]:
if data is None:
return None
return entry._get_member(int(data))
return entry._state._get_guild(data)
def _transform_overwrites(entry, data):
def _transform_overwrites(
entry: AuditLogEntry, data: List[PermissionOverwritePayload]
) -> List[Tuple[Object, PermissionOverwrite]]:
overwrites = []
for elem in data:
allow = Permissions(elem['allow'])
deny = Permissions(elem['deny'])
allow = Permissions(int(elem["allow"]))
deny = Permissions(int(elem["deny"]))
ow = PermissionOverwrite.from_pair(allow, deny)
ow_type = elem['type']
ow_id = int(elem['id'])
if ow_type == 'role':
ow_type = elem["type"]
ow_id = int(elem["id"])
target = None
if ow_type == "0":
target = entry.guild.get_role(ow_id)
else:
elif ow_type == "1":
target = entry._get_member(ow_id)
if target is None:
@ -89,63 +116,132 @@ def _transform_overwrites(entry, data):
return overwrites
def _transform_icon(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
if data is None:
return None
return Asset._from_guild_icon(entry._state, entry.guild.id, data)
def _transform_avatar(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
if data is None:
return None
return Asset._from_avatar(entry._state, entry._target_id, data) # type: ignore
def _guild_hash_transformer(path: str) -> Callable[[AuditLogEntry, Optional[str]], Optional[Asset]]:
def _transform(entry: AuditLogEntry, data: Optional[str]) -> Optional[Asset]:
if data is None:
return None
return Asset._from_guild_image(entry._state, entry.guild.id, data, path=path)
return _transform
T = TypeVar("T", bound=enums.Enum)
def _enum_transformer(enum: Type[T]) -> Callable[[AuditLogEntry, int], T]:
def _transform(entry: AuditLogEntry, data: int) -> T:
return enums.try_enum(enum, data)
return _transform
def _transform_type(entry: AuditLogEntry, data: Union[int]) -> Union[enums.ChannelType, enums.StickerType]:
if entry.action.name.startswith("sticker_"):
return enums.try_enum(enums.StickerType, data)
else:
return enums.try_enum(enums.ChannelType, data)
class AuditLogDiff:
def __len__(self):
def __len__(self) -> int:
return len(self.__dict__)
def __iter__(self):
return iter(self.__dict__.items())
def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
yield from self.__dict__.items()
def __repr__(self) -> str:
values = " ".join("%s=%r" % item for item in self.__dict__.items())
return f"<AuditLogDiff {values}>"
if TYPE_CHECKING:
def __getattr__(self, item: str) -> Any:
...
def __setattr__(self, key: str, value: Any) -> Any:
...
Transformer = Callable[["AuditLogEntry", Any], Any]
def __repr__(self):
values = ' '.join('%s=%r' % item for item in self.__dict__.items())
return f'<AuditLogDiff {values}>'
class AuditLogChanges:
TRANSFORMERS = {
'verification_level': (None, _transform_verification_level),
'explicit_content_filter': (None, _transform_explicit_content_filter),
# fmt: off
TRANSFORMERS: ClassVar[Dict[str, Tuple[Optional[str], Optional[Transformer]]]] = {
'verification_level': (None, _enum_transformer(enums.VerificationLevel)),
'explicit_content_filter': (None, _enum_transformer(enums.ContentFilter)),
'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),
'owner_id': ('owner', _transform_member_id),
'inviter_id': ('inviter', _transform_member_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),
'rules_channel_id': ('rules_channel', _transform_channel),
'public_updates_channel_id': ('public_updates_channel', _transform_channel),
'permission_overwrites': ('overwrites', _transform_overwrites),
'splash_hash': ('splash', None),
'icon_hash': ('icon', None),
'avatar_hash': ('avatar', None),
'splash_hash': ('splash', _guild_hash_transformer('splashes')),
'banner_hash': ('banner', _guild_hash_transformer('banners')),
'discovery_splash_hash': ('discovery_splash', _guild_hash_transformer('discovery-splashes')),
'icon_hash': ('icon', _transform_icon),
'avatar_hash': ('avatar', _transform_avatar),
'rate_limit_per_user': ('slowmode_delay', None),
'default_message_notifications': ('default_notifications', _transform_default_notifications),
'guild_id': ('guild', _transform_guild_id),
'tags': ('emoji', None),
'default_message_notifications': ('default_notifications', _enum_transformer(enums.NotificationLevel)),
'region': (None, _enum_transformer(enums.VoiceRegion)),
'rtc_region': (None, _enum_transformer(enums.VoiceRegion)),
'video_quality_mode': (None, _enum_transformer(enums.VideoQualityMode)),
'privacy_level': (None, _enum_transformer(enums.StagePrivacyLevel)),
'format_type': (None, _enum_transformer(enums.StickerFormatType)),
'type': (None, _transform_type),
}
# fmt: on
def __init__(self, entry, data):
def __init__(self, entry: AuditLogEntry, data: List[AuditLogChangePayload]):
self.before = AuditLogDiff()
self.after = AuditLogDiff()
for elem in data:
attr = elem['key']
attr = elem["key"]
# special cases for role add/remove
if attr == '$add':
self._handle_role(self.before, self.after, entry, elem['new_value'])
if attr == "$add":
self._handle_role(self.before, self.after, entry, elem["new_value"]) # type: ignore
continue
elif attr == '$remove':
self._handle_role(self.after, self.before, entry, elem['new_value'])
elif attr == "$remove":
self._handle_role(self.after, self.before, entry, elem["new_value"]) # type: ignore
continue
transformer = self.TRANSFORMERS.get(attr)
if transformer:
key, transformer = transformer
try:
key, transformer = self.TRANSFORMERS[attr]
except (ValueError, KeyError):
transformer = None
else:
if key:
attr = key
transformer: Optional[Transformer]
try:
before = elem['old_value']
before = elem["old_value"]
except KeyError:
before = None
else:
@ -155,7 +251,7 @@ class AuditLogChanges:
setattr(self.before, attr, before)
try:
after = elem['new_value']
after = elem["new_value"]
except KeyError:
after = None
else:
@ -165,31 +261,60 @@ class AuditLogChanges:
setattr(self.after, attr, after)
# add an alias
if hasattr(self.after, 'colour'):
if hasattr(self.after, "colour"):
self.after.color = self.after.colour
self.before.color = self.before.colour
if hasattr(self.after, "expire_behavior"):
self.after.expire_behaviour = self.after.expire_behavior
self.before.expire_behaviour = self.before.expire_behavior
def __repr__(self):
return f'<AuditLogChanges before={self.before!r} after={self.after!r}>'
def __repr__(self) -> str:
return f"<AuditLogChanges before={self.before!r} after={self.after!r}>"
def _handle_role(self, first, second, entry, elem):
if not hasattr(first, 'roles'):
setattr(first, 'roles', [])
def _handle_role(
self, first: AuditLogDiff, second: AuditLogDiff, entry: AuditLogEntry, elem: List[RolePayload]
) -> None:
if not hasattr(first, "roles"):
setattr(first, "roles", [])
data = []
g = entry.guild
g: Guild = entry.guild # type: ignore
for e in elem:
role_id = int(e['id'])
role_id = int(e["id"])
role = g.get_role(role_id)
if role is None:
role = Object(id=role_id)
role.name = e['name']
role.name = e["name"] # type: ignore
data.append(role)
setattr(second, 'roles', data)
setattr(second, "roles", data)
class _AuditLogProxyMemberPrune:
delete_member_days: int
members_removed: int
class _AuditLogProxyMemberMoveOrMessageDelete:
channel: abc.GuildChannel
count: int
class _AuditLogProxyMemberDisconnect:
count: int
class _AuditLogProxyPinAction:
channel: abc.GuildChannel
message_id: int
class _AuditLogProxyStageInstanceAction:
channel: abc.GuildChannel
class AuditLogEntry(Hashable):
r"""Represents an Audit Log entry.
@ -210,6 +335,10 @@ class AuditLogEntry(Hashable):
Returns the entry's hash.
.. describe:: int(x)
Returns the entry's ID.
.. versionchanged:: 1.7
Audit log entries are now comparable and hashable.
@ -234,153 +363,175 @@ class AuditLogEntry(Hashable):
which actions have this field filled out.
"""
def __init__(self, *, users, data, guild):
def __init__(self, *, users: Dict[int, User], data: AuditLogEntryPayload, guild: Guild):
self._state = guild._state
self.guild = guild
self._users = users
self._from_data(data)
def _from_data(self, data):
self.action = enums.try_enum(enums.AuditLogAction, data['action_type'])
self.id = int(data['id'])
def _from_data(self, data: AuditLogEntryPayload) -> None:
self.action = enums.try_enum(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')
self.reason = data.get("reason")
self.extra = data.get("options")
if isinstance(self.action, enums.AuditLogAction) and 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()})()
self.extra: _AuditLogProxyMemberPrune = type(
"_AuditLogProxy", (), {k: int(v) for k, v in self.extra.items()}
)()
elif self.action is enums.AuditLogAction.member_move or self.action is enums.AuditLogAction.message_delete:
channel_id = int(self.extra['channel_id'])
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)
"count": int(self.extra["count"]),
"channel": self.guild.get_channel(channel_id) or Object(id=channel_id),
}
self.extra = type('_AuditLogProxy', (), elems)()
self.extra: _AuditLogProxyMemberMoveOrMessageDelete = type("_AuditLogProxy", (), elems)()
elif self.action is enums.AuditLogAction.member_disconnect:
# The member disconnect action has a dict with some information
elems = {
'count': int(self.extra['count']),
"count": int(self.extra["count"]),
}
self.extra = type('_AuditLogProxy', (), elems)()
elif self.action.name.endswith('pin'):
self.extra: _AuditLogProxyMemberDisconnect = type("_AuditLogProxy", (), elems)()
elif self.action.name.endswith("pin"):
# the pin actions have a dict with some information
channel_id = int(self.extra['channel_id'])
message_id = int(self.extra['message_id'])
channel_id = int(self.extra["channel_id"])
elems = {
'channel': self.guild.get_channel(channel_id) or Object(id=channel_id),
'message_id': message_id
"channel": self.guild.get_channel(channel_id) or Object(id=channel_id),
"message_id": int(self.extra["message_id"]),
}
self.extra = type('_AuditLogProxy', (), elems)()
elif self.action.name.startswith('overwrite_'):
self.extra: _AuditLogProxyPinAction = 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':
instance_id = int(self.extra["id"])
the_type = self.extra.get("type")
if the_type == "1":
self.extra = self._get_member(instance_id)
else:
elif the_type == "0":
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
role.name = self.extra.get("role_name") # type: ignore
self.extra: Role = role
elif self.action.name.startswith("stage_instance"):
channel_id = int(self.extra["channel_id"])
elems = {"channel": self.guild.get_channel(channel_id) or Object(id=channel_id)}
self.extra: _AuditLogProxyStageInstanceAction = type("_AuditLogProxy", (), elems)()
# fmt: off
self.extra: Union[
_AuditLogProxyMemberPrune,
_AuditLogProxyMemberMoveOrMessageDelete,
_AuditLogProxyMemberDisconnect,
_AuditLogProxyPinAction,
_AuditLogProxyStageInstanceAction,
Member, User, None,
Role,
]
# fmt: on
# 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._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')
self.user = self._get_member(utils._get_as_snowflake(data, "user_id")) # type: ignore
self._target_id = utils._get_as_snowflake(data, "target_id")
def _get_member(self, user_id):
def _get_member(self, user_id: int) -> Union[Member, User, None]:
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)
def __repr__(self) -> str:
return f"<AuditLogEntry id={self.id} action={self.action} user={self.user!r}>"
@utils.cached_property
def created_at(self):
def created_at(self) -> datetime.datetime:
""":class:`datetime.datetime`: Returns the entry's creation time in UTC."""
return utils.snowflake_time(self.id)
@utils.cached_property
def target(self):
def target(
self,
) -> Union[
Guild, abc.GuildChannel, Member, User, Role, Invite, Emoji, StageInstance, GuildSticker, Thread, Object, None
]:
try:
converter = getattr(self, '_convert_target_' + self.action.target_type)
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):
def category(self) -> enums.AuditLogActionCategory:
"""Optional[:class:`AuditLogActionCategory`]: The category of the action, if applicable."""
return self.action.category
@utils.cached_property
def changes(self):
def changes(self) -> AuditLogChanges:
""":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):
def before(self) -> AuditLogDiff:
""":class:`AuditLogDiff`: The target's prior state."""
return self.changes.before
@utils.cached_property
def after(self):
def after(self) -> AuditLogDiff:
""":class:`AuditLogDiff`: The target's subsequent state."""
return self.changes.after
def _convert_target_guild(self, target_id):
def _convert_target_guild(self, target_id: int) -> Guild:
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_channel(self, target_id: int) -> Union[abc.GuildChannel, Object]:
return self.guild.get_channel(target_id) or Object(id=target_id)
def _convert_target_user(self, target_id):
def _convert_target_user(self, target_id: int) -> Union[Member, User, None]:
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_role(self, target_id: int) -> Union[Role, Object]:
return self.guild.get_role(target_id) or Object(id=target_id)
def _convert_target_invite(self, target_id):
def _convert_target_invite(self, target_id: int) -> Invite:
# 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,
"max_age": changeset.max_age,
"max_uses": changeset.max_uses,
"code": changeset.code,
"temporary": changeset.temporary,
"uses": changeset.uses,
}
obj = Invite(state=self._state, data=fake_payload)
obj = Invite(state=self._state, data=fake_payload, guild=self.guild, channel=changeset.channel) # type: ignore
try:
obj.inviter = changeset.inviter
except AttributeError:
pass
return obj
def _convert_target_emoji(self, target_id):
def _convert_target_emoji(self, target_id: int) -> Union[Emoji, Object]:
return self._state.get_emoji(target_id) or Object(id=target_id)
def _convert_target_message(self, target_id):
def _convert_target_message(self, target_id: int) -> Union[Member, User, None]:
return self._get_member(target_id)
def _convert_target_stage_instance(self, target_id: int) -> Union[StageInstance, Object]:
return self.guild.get_stage_instance(target_id) or Object(id=target_id)
def _convert_target_sticker(self, target_id: int) -> Union[GuildSticker, Object]:
return self._state.get_sticker(target_id) or Object(id=target_id)
def _convert_target_thread(self, target_id: int) -> Union[Thread, Object]:
return self.guild.get_thread(target_id) or Object(id=target_id)

View File

@ -22,14 +22,19 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import time
import random
from typing import Callable, Generic, Literal, TypeVar, overload, Union
__all__ = (
'ExponentialBackoff',
)
T = TypeVar("T", bool, Literal[True], Literal[False])
class ExponentialBackoff:
__all__ = ("ExponentialBackoff",)
class ExponentialBackoff(Generic[T]):
"""An implementation of the exponential backoff algorithm
Provides a convenient interface to implement an exponential backoff
@ -51,21 +56,33 @@ class ExponentialBackoff:
number in between may be returned.
"""
def __init__(self, base=1, *, integral=False):
self._base = base
def __init__(self, base: int = 1, *, integral: T = False):
self._base: int = base
self._exp = 0
self._max = 10
self._reset_time = base * 2 ** 11
self._last_invocation = time.monotonic()
self._exp: int = 0
self._max: int = 10
self._reset_time: int = base * 2 ** 11
self._last_invocation: float = 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
self._randfunc: Callable[..., Union[int, float]] = rand.randrange if integral else rand.uniform # type: ignore
def delay(self):
@overload
def delay(self: ExponentialBackoff[Literal[False]]) -> float:
...
@overload
def delay(self: ExponentialBackoff[Literal[True]]) -> int:
...
@overload
def delay(self: ExponentialBackoff[bool]) -> Union[int, float]:
...
def delay(self) -> Union[int, float]:
"""Compute the next delay
Returns the next delay to wait according to the exponential

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -25,11 +25,23 @@ DEALINGS IN THE SOFTWARE.
import colorsys
import random
__all__ = (
'Colour',
'Color',
from typing import (
Any,
Optional,
Tuple,
Type,
TypeVar,
Union,
)
__all__ = (
"Colour",
"Color",
)
CT = TypeVar("CT", bound="Colour")
class Colour:
"""Represents a Discord role colour. This class is similar
to a (red, green, blue) :class:`tuple`.
@ -54,75 +66,82 @@ class Colour:
Returns the hex format for the colour.
.. describe:: int(x)
Returns the raw colour value.
Attributes
------------
value: :class:`int`
The raw integer colour value.
"""
__slots__ = ('value',)
__slots__ = ("value",)
def __init__(self, value):
def __init__(self, value: int):
if not isinstance(value, int):
raise TypeError(f'Expected int parameter, received {value.__class__.__name__} instead.')
raise TypeError(f"Expected int parameter, received {value.__class__.__name__} instead.")
self.value = value
self.value: int = value
def _get_byte(self, byte):
return (self.value >> (8 * byte)) & 0xff
def _get_byte(self, byte: int) -> int:
return (self.value >> (8 * byte)) & 0xFF
def __eq__(self, other):
def __eq__(self, other: Any) -> bool:
return isinstance(other, Colour) and self.value == other.value
def __ne__(self, other):
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __str__(self):
return f'#{self.value:0>6x}'
def __str__(self) -> str:
return f"#{self.value:0>6x}"
def __repr__(self):
return f'<Colour value={self.value}>'
def __int__(self) -> int:
return self.value
def __hash__(self):
def __repr__(self) -> str:
return f"<Colour value={self.value}>"
def __hash__(self) -> int:
return hash(self.value)
@property
def r(self):
def r(self) -> int:
""":class:`int`: Returns the red component of the colour."""
return self._get_byte(2)
@property
def g(self):
def g(self) -> int:
""":class:`int`: Returns the green component of the colour."""
return self._get_byte(1)
@property
def b(self):
def b(self) -> int:
""":class:`int`: Returns the blue component of the colour."""
return self._get_byte(0)
def to_rgb(self):
def to_rgb(self) -> Tuple[int, int, int]:
"""Tuple[:class:`int`, :class:`int`, :class:`int`]: Returns an (r, g, b) tuple representing the colour."""
return (self.r, self.g, self.b)
@classmethod
def from_rgb(cls, r, g, b):
def from_rgb(cls: Type[CT], r: int, g: int, b: int) -> CT:
"""Constructs a :class:`Colour` from an RGB tuple."""
return cls((r << 16) + (g << 8) + b)
@classmethod
def from_hsv(cls, h, s, v):
def from_hsv(cls: Type[CT], h: float, s: float, v: float) -> CT:
"""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):
def default(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0``."""
return cls(0)
@classmethod
def random(cls, *, seed=None):
def random(cls: Type[CT], *, seed: Optional[Union[int, str, float, bytes, bytearray]] = None) -> CT:
"""A factory method that returns a :class:`Colour` with a random hue.
.. note::
@ -143,125 +162,153 @@ class Colour:
return cls.from_hsv(rand.random(), 1, 1)
@classmethod
def teal(cls):
def teal(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x1abc9c``."""
return cls(0x1abc9c)
return cls(0x1ABC9C)
@classmethod
def dark_teal(cls):
def dark_teal(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x11806a``."""
return cls(0x11806a)
return cls(0x11806A)
@classmethod
def green(cls):
def brand_green(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x57F287``.
.. versionadded:: 2.0
"""
return cls(0x57F287)
@classmethod
def green(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x2ecc71``."""
return cls(0x2ecc71)
return cls(0x2ECC71)
@classmethod
def dark_green(cls):
def dark_green(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x1f8b4c``."""
return cls(0x1f8b4c)
return cls(0x1F8B4C)
@classmethod
def blue(cls):
def blue(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x3498db``."""
return cls(0x3498db)
return cls(0x3498DB)
@classmethod
def dark_blue(cls):
def dark_blue(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x206694``."""
return cls(0x206694)
@classmethod
def purple(cls):
def purple(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x9b59b6``."""
return cls(0x9b59b6)
return cls(0x9B59B6)
@classmethod
def dark_purple(cls):
def dark_purple(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x71368a``."""
return cls(0x71368a)
return cls(0x71368A)
@classmethod
def magenta(cls):
def magenta(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0xe91e63``."""
return cls(0xe91e63)
return cls(0xE91E63)
@classmethod
def dark_magenta(cls):
def dark_magenta(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0xad1457``."""
return cls(0xad1457)
return cls(0xAD1457)
@classmethod
def gold(cls):
def gold(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0xf1c40f``."""
return cls(0xf1c40f)
return cls(0xF1C40F)
@classmethod
def dark_gold(cls):
def dark_gold(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0xc27c0e``."""
return cls(0xc27c0e)
return cls(0xC27C0E)
@classmethod
def orange(cls):
def orange(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0xe67e22``."""
return cls(0xe67e22)
return cls(0xE67E22)
@classmethod
def dark_orange(cls):
def dark_orange(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0xa84300``."""
return cls(0xa84300)
return cls(0xA84300)
@classmethod
def red(cls):
def brand_red(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0xED4245``.
.. versionadded:: 2.0
"""
return cls(0xED4245)
@classmethod
def red(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``."""
return cls(0xe74c3c)
return cls(0xE74C3C)
@classmethod
def dark_red(cls):
def nitro_booster(cls):
"""A factory method that returns a :class:`Colour` with a value of ``0xf47fff``.
.. versionadded:: 2.0"""
return cls(0xF47FFF)
@classmethod
def dark_red(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x992d22``."""
return cls(0x992d22)
return cls(0x992D22)
@classmethod
def lighter_grey(cls):
def lighter_grey(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x95a5a6``."""
return cls(0x95a5a6)
return cls(0x95A5A6)
lighter_gray = lighter_grey
@classmethod
def dark_grey(cls):
def dark_grey(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x607d8b``."""
return cls(0x607d8b)
return cls(0x607D8B)
dark_gray = dark_grey
@classmethod
def light_grey(cls):
def light_grey(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x979c9f``."""
return cls(0x979c9f)
return cls(0x979C9F)
light_gray = light_grey
@classmethod
def darker_grey(cls):
def darker_grey(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x546e7a``."""
return cls(0x546e7a)
return cls(0x546E7A)
darker_gray = darker_grey
@classmethod
def blurple(cls):
def og_blurple(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x7289da``."""
return cls(0x7289da)
return cls(0x7289DA)
@classmethod
def greyple(cls):
def blurple(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x5865F2``."""
return cls(0x5865F2)
@classmethod
def greyple(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x99aab5``."""
return cls(0x99aab5)
return cls(0x99AAB5)
@classmethod
def dark_theme(cls):
def dark_theme(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x36393F``.
This will appear transparent on Discord's dark theme.
@ -269,4 +316,30 @@ class Colour:
"""
return cls(0x36393F)
@classmethod
def fuchsia(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0xEB459E``.
.. versionadded:: 2.0
"""
return cls(0xEB459E)
@classmethod
def yellow(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0xFEE75C``.
.. versionadded:: 2.0
"""
return cls(0xFEE75C)
@classmethod
def dark_blurple(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0x4E5D94``.
This is the original Dark Blurple branding.
.. versionadded:: 2.0
"""
return cls(0x4E5D94)
Color = Colour

383
discord/components.py Normal file
View File

@ -0,0 +1,383 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union
from .enums import try_enum, ComponentType, ButtonStyle
from .utils import get_slots, MISSING
from .partial_emoji import PartialEmoji, _EmojiTag
if TYPE_CHECKING:
from .types.components import (
Component as ComponentPayload,
ButtonComponent as ButtonComponentPayload,
SelectMenu as SelectMenuPayload,
SelectOption as SelectOptionPayload,
ActionRow as ActionRowPayload,
)
from .emoji import Emoji
__all__ = (
"Component",
"ActionRow",
"Button",
"SelectMenu",
"SelectOption",
)
C = TypeVar("C", bound="Component")
class Component:
"""Represents a Discord Bot UI Kit Component.
Currently, the only components supported by Discord are:
- :class:`ActionRow`
- :class:`Button`
- :class:`SelectMenu`
This class is abstract and cannot be instantiated.
.. versionadded:: 2.0
Attributes
------------
type: :class:`ComponentType`
The type of component.
"""
__slots__: Tuple[str, ...] = ("type",)
__repr_info__: ClassVar[Tuple[str, ...]]
type: ComponentType
def __repr__(self) -> str:
attrs = " ".join(f"{key}={getattr(self, key)!r}" for key in self.__repr_info__)
return f"<{self.__class__.__name__} {attrs}>"
@classmethod
def _raw_construct(cls: Type[C], **kwargs) -> C:
self: C = cls.__new__(cls)
for slot in get_slots(cls):
try:
value = kwargs[slot]
except KeyError:
pass
else:
setattr(self, slot, value)
return self
def to_dict(self) -> Dict[str, Any]:
raise NotImplementedError
class ActionRow(Component):
"""Represents a Discord Bot UI Kit Action Row.
This is a component that holds up to 5 children components in a row.
This inherits from :class:`Component`.
.. versionadded:: 2.0
Attributes
------------
type: :class:`ComponentType`
The type of component.
children: List[:class:`Component`]
The children components that this holds, if any.
"""
__slots__: Tuple[str, ...] = ("children",)
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
def __init__(self, data: ComponentPayload):
self.type: ComponentType = try_enum(ComponentType, data["type"])
self.children: List[Component] = [_component_factory(d) for d in data.get("components", [])]
def to_dict(self) -> ActionRowPayload:
return {
"type": int(self.type),
"components": [child.to_dict() for child in self.children],
} # type: ignore
class Button(Component):
"""Represents a button from the Discord Bot UI Kit.
This inherits from :class:`Component`.
.. note::
The user constructible and usable type to create a button is :class:`discord.ui.Button`
not this one.
.. versionadded:: 2.0
Attributes
-----------
style: :class:`.ButtonStyle`
The style of the button.
custom_id: Optional[:class:`str`]
The ID of the button that gets received during an interaction.
If this button is for a URL, it does not have a custom ID.
url: Optional[:class:`str`]
The URL this button sends you to.
disabled: :class:`bool`
Whether the button is disabled or not.
label: Optional[:class:`str`]
The label of the button, if any.
emoji: Optional[:class:`PartialEmoji`]
The emoji of the button, if available.
"""
__slots__: Tuple[str, ...] = (
"style",
"custom_id",
"url",
"disabled",
"label",
"emoji",
)
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
def __init__(self, data: ButtonComponentPayload):
self.type: ComponentType = try_enum(ComponentType, data["type"])
self.style: ButtonStyle = try_enum(ButtonStyle, data["style"])
self.custom_id: Optional[str] = data.get("custom_id")
self.url: Optional[str] = data.get("url")
self.disabled: bool = data.get("disabled", False)
self.label: Optional[str] = data.get("label")
self.emoji: Optional[PartialEmoji]
try:
self.emoji = PartialEmoji.from_dict(data["emoji"])
except KeyError:
self.emoji = None
def to_dict(self) -> ButtonComponentPayload:
payload = {
"type": 2,
"style": int(self.style),
"label": self.label,
"disabled": self.disabled,
}
if self.custom_id:
payload["custom_id"] = self.custom_id
if self.url:
payload["url"] = self.url
if self.emoji:
payload["emoji"] = self.emoji.to_dict()
return payload # type: ignore
class SelectMenu(Component):
"""Represents a select menu from the Discord Bot UI Kit.
A select menu is functionally the same as a dropdown, however
on mobile it renders a bit differently.
.. note::
The user constructible and usable type to create a select menu is
:class:`discord.ui.Select` not this one.
.. versionadded:: 2.0
Attributes
------------
custom_id: Optional[:class:`str`]
The ID of the select menu that gets received during an interaction.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
max_values: :class:`int`
The maximum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
options: List[:class:`SelectOption`]
A list of options that can be selected in this menu.
disabled: :class:`bool`
Whether the select is disabled or not.
"""
__slots__: Tuple[str, ...] = (
"custom_id",
"placeholder",
"min_values",
"max_values",
"options",
"disabled",
)
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
def __init__(self, data: SelectMenuPayload):
self.type = ComponentType.select
self.custom_id: str = data["custom_id"]
self.placeholder: Optional[str] = data.get("placeholder")
self.min_values: int = data.get("min_values", 1)
self.max_values: int = data.get("max_values", 1)
self.options: List[SelectOption] = [SelectOption.from_dict(option) for option in data.get("options", [])]
self.disabled: bool = data.get("disabled", False)
def to_dict(self) -> SelectMenuPayload:
payload: SelectMenuPayload = {
"type": self.type.value,
"custom_id": self.custom_id,
"min_values": self.min_values,
"max_values": self.max_values,
"options": [op.to_dict() for op in self.options],
"disabled": self.disabled,
}
if self.placeholder:
payload["placeholder"] = self.placeholder
return payload
class SelectOption:
"""Represents a select menu's option.
These can be created by users.
.. versionadded:: 2.0
Attributes
-----------
label: :class:`str`
The label of the option. This is displayed to users.
Can only be up to 100 characters.
value: :class:`str`
The value of the option. This is not displayed to users.
If not provided when constructed then it defaults to the
label. Can only be up to 100 characters.
description: Optional[:class:`str`]
An additional description of the option, if any.
Can only be up to 100 characters.
emoji: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]]
The emoji of the option, if available.
default: :class:`bool`
Whether this option is selected by default.
"""
__slots__: Tuple[str, ...] = (
"label",
"value",
"description",
"emoji",
"default",
)
def __init__(
self,
*,
label: str,
value: str = MISSING,
description: Optional[str] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
default: bool = False,
) -> None:
self.label = label
self.value = label if value is MISSING else value
self.description = description
if emoji is not None:
if isinstance(emoji, str):
emoji = PartialEmoji.from_str(emoji)
elif isinstance(emoji, _EmojiTag):
emoji = emoji._to_partial()
else:
raise TypeError(f"expected emoji to be str, Emoji, or PartialEmoji not {emoji.__class__}")
self.emoji = emoji
self.default = default
def __repr__(self) -> str:
return (
f"<SelectOption label={self.label!r} value={self.value!r} description={self.description!r} "
f"emoji={self.emoji!r} default={self.default!r}>"
)
def __str__(self) -> str:
if self.emoji:
base = f"{self.emoji} {self.label}"
else:
base = self.label
if self.description:
return f"{base}\n{self.description}"
return base
@classmethod
def from_dict(cls, data: SelectOptionPayload) -> SelectOption:
try:
emoji = PartialEmoji.from_dict(data["emoji"])
except KeyError:
emoji = None
return cls(
label=data["label"],
value=data["value"],
description=data.get("description"),
emoji=emoji,
default=data.get("default", False),
)
def to_dict(self) -> SelectOptionPayload:
payload: SelectOptionPayload = {
"label": self.label,
"value": self.value,
"default": self.default,
}
if self.emoji:
payload["emoji"] = self.emoji.to_dict() # type: ignore
if self.description:
payload["description"] = self.description
return payload
def _component_factory(data: ComponentPayload) -> Component:
component_type = data["type"]
if component_type == 1:
return ActionRow(data)
elif component_type == 2:
return Button(data) # type: ignore
elif component_type == 3:
return SelectMenu(data) # type: ignore
else:
as_enum = try_enum(ComponentType, component_type)
return Component._raw_construct(type=as_enum)

View File

@ -22,25 +22,35 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING, TypeVar, Optional, Type
__all__ = (
'Typing',
)
if TYPE_CHECKING:
from .abc import Messageable
def _typing_done_callback(fut):
from types import TracebackType
TypingT = TypeVar("TypingT", bound="Typing")
__all__ = ("Typing",)
def _typing_done_callback(fut: asyncio.Future) -> None:
# just retrieve any exception and call it a day
try:
fut.exception()
except (asyncio.CancelledError, Exception):
pass
class Typing:
def __init__(self, messageable):
self.loop = messageable._state.loop
self.messageable = messageable
async def do_typing(self):
class Typing:
def __init__(self, messageable: Messageable) -> None:
self.loop: asyncio.AbstractEventLoop = messageable._state.loop
self.messageable: Messageable = messageable
async def do_typing(self) -> None:
try:
channel = self._channel
except AttributeError:
@ -52,18 +62,28 @@ class Typing:
await typing(channel.id)
await asyncio.sleep(5)
def __enter__(self):
self.task = asyncio.ensure_future(self.do_typing(), loop=self.loop)
def __enter__(self: TypingT) -> TypingT:
self.task: asyncio.Task = self.loop.create_task(self.do_typing())
self.task.add_done_callback(_typing_done_callback)
return self
def __exit__(self, exc_type, exc, tb):
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
self.task.cancel()
async def __aenter__(self):
async def __aenter__(self: TypingT) -> TypingT:
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):
async def __aexit__(
self,
exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
self.task.cancel()

View File

@ -25,14 +25,12 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
import datetime
from typing import Any, Dict, Final, List, Protocol, TYPE_CHECKING, Type, TypeVar, Union
from typing import Any, Dict, Final, List, Mapping, Protocol, TYPE_CHECKING, Type, TypeVar, Union
from . import utils
from .colour import Colour
__all__ = (
'Embed',
)
__all__ = ("Embed",)
class _EmptyEmbed:
@ -40,7 +38,7 @@ class _EmptyEmbed:
return False
def __repr__(self) -> str:
return 'Embed.Empty'
return "Embed.Empty"
def __len__(self) -> int:
return 0
@ -57,19 +55,19 @@ class EmbedProxy:
return len(self.__dict__)
def __repr__(self) -> str:
inner = ', '.join((f'{k}={v!r}' for k, v in self.__dict__.items() if not k.startswith('_')))
return f'EmbedProxy({inner})'
inner = ", ".join((f"{k}={v!r}" for k, v in self.__dict__.items() if not k.startswith("_")))
return f"EmbedProxy({inner})"
def __getattr__(self, attr: str) -> _EmptyEmbed:
return EmptyEmbed
E = TypeVar('E', bound='Embed')
E = TypeVar("E", bound="Embed")
if TYPE_CHECKING:
from discord.types.embed import Embed as EmbedData, EmbedType
T = TypeVar('T')
T = TypeVar("T")
MaybeEmpty = Union[T, _EmptyEmbed]
class _EmbedFooterProxy(Protocol):
@ -157,19 +155,19 @@ class Embed:
"""
__slots__ = (
'title',
'url',
'type',
'_timestamp',
'_colour',
'_footer',
'_image',
'_thumbnail',
'_video',
'_provider',
'_author',
'_fields',
'description',
"title",
"url",
"type",
"_timestamp",
"_colour",
"_footer",
"_image",
"_thumbnail",
"_video",
"_provider",
"_author",
"_fields",
"description",
)
Empty: Final = EmptyEmbed
@ -179,10 +177,10 @@ class Embed:
*,
colour: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
color: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
title: MaybeEmpty[str] = EmptyEmbed,
type: EmbedType = 'rich',
url: MaybeEmpty[str] = EmptyEmbed,
description: MaybeEmpty[str] = EmptyEmbed,
title: MaybeEmpty[Any] = EmptyEmbed,
type: EmbedType = "rich",
url: MaybeEmpty[Any] = EmptyEmbed,
description: MaybeEmpty[Any] = EmptyEmbed,
timestamp: datetime.datetime = None,
):
@ -202,12 +200,10 @@ class Embed:
self.url = str(self.url)
if timestamp:
if timestamp.tzinfo is None:
timestamp = timestamp.astimezone()
self.timestamp = timestamp
@classmethod
def from_dict(cls: Type[E], data: EmbedData) -> E:
def from_dict(cls: Type[E], data: Mapping[str, Any]) -> E:
"""Converts a :class:`dict` to a :class:`Embed` provided it is in the
format that Discord expects it to be in.
@ -227,10 +223,10 @@ class Embed:
# 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)
self.title = data.get("title", EmptyEmbed)
self.type = data.get("type", EmptyEmbed)
self.description = data.get("description", EmptyEmbed)
self.url = data.get("url", EmptyEmbed)
if self.title is not EmptyEmbed:
self.title = str(self.title)
@ -244,22 +240,22 @@ class Embed:
# try to fill in the more rich fields
try:
self._colour = Colour(value=data['color'])
self._colour = Colour(value=data["color"])
except KeyError:
pass
try:
self._timestamp = utils.parse_time(data['timestamp'])
self._timestamp = utils.parse_time(data["timestamp"])
except KeyError:
pass
for attr in ('thumbnail', 'video', 'provider', 'author', 'fields', 'image', 'footer'):
for attr in ("thumbnail", "video", "provider", "author", "fields", "image", "footer"):
try:
value = data[attr]
except KeyError:
continue
else:
setattr(self, '_' + attr, value)
setattr(self, "_" + attr, value)
return self
@ -269,22 +265,22 @@ class Embed:
def __len__(self) -> int:
total = len(self.title) + len(self.description)
for field in getattr(self, '_fields', []):
total += len(field['name']) + len(field['value'])
for field in getattr(self, "_fields", []):
total += len(field["name"]) + len(field["value"])
try:
footer = self._footer
except AttributeError:
footer_text = self._footer["text"]
except (AttributeError, KeyError):
pass
else:
total += len(footer['text'])
total += len(footer_text)
try:
author = self._author
except AttributeError:
pass
else:
total += len(author['name'])
total += len(author["name"])
return total
@ -308,7 +304,7 @@ class Embed:
@property
def colour(self) -> MaybeEmpty[Colour]:
return getattr(self, '_colour', EmptyEmbed)
return getattr(self, "_colour", EmptyEmbed)
@colour.setter
def colour(self, value: Union[int, Colour, _EmptyEmbed]): # type: ignore
@ -317,17 +313,23 @@ class Embed:
elif isinstance(value, int):
self._colour = Colour(value=value)
else:
raise TypeError(f'Expected discord.Colour, int, or Embed.Empty but received {value.__class__.__name__} instead.')
raise TypeError(
f"Expected discord.Colour, int, or Embed.Empty but received {value.__class__.__name__} instead."
)
color = colour
@property
def timestamp(self) -> MaybeEmpty[datetime.datetime]:
return getattr(self, '_timestamp', EmptyEmbed)
return getattr(self, "_timestamp", EmptyEmbed)
@timestamp.setter
def timestamp(self, value: MaybeEmpty[datetime.datetime]):
if isinstance(value, (datetime.datetime, _EmptyEmbed)):
if isinstance(value, datetime.datetime):
if value.tzinfo is None:
value = value.astimezone()
self._timestamp = value
elif isinstance(value, _EmptyEmbed):
self._timestamp = value
else:
raise TypeError(f"Expected datetime.datetime or Embed.Empty received {value.__class__.__name__} instead")
@ -340,9 +342,9 @@ class Embed:
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_footer', {})) # type: ignore
return EmbedProxy(getattr(self, "_footer", {})) # type: ignore
def set_footer(self: E, *, text: MaybeEmpty[str] = EmptyEmbed, icon_url: MaybeEmpty[str] = EmptyEmbed) -> E:
def set_footer(self: E, *, text: MaybeEmpty[Any] = EmptyEmbed, icon_url: MaybeEmpty[Any] = EmptyEmbed) -> E:
"""Sets the footer for the embed content.
This function returns the class instance to allow for fluent-style
@ -358,10 +360,25 @@ class Embed:
self._footer = {}
if text is not EmptyEmbed:
self._footer['text'] = str(text)
self._footer["text"] = str(text)
if icon_url is not EmptyEmbed:
self._footer['icon_url'] = str(icon_url)
self._footer["icon_url"] = str(icon_url)
return self
def remove_footer(self: E) -> E:
"""Clears embed's footer information.
This function returns the class instance to allow for fluent-style
chaining.
.. versionadded:: 2.0
"""
try:
del self._footer
except AttributeError:
pass
return self
@ -378,9 +395,23 @@ class Embed:
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_image', {})) # type: ignore
return EmbedProxy(getattr(self, "_image", {})) # type: ignore
def set_image(self: E, *, url: MaybeEmpty[str]) -> E:
@image.setter
def image(self, url: Any):
if url is EmptyEmbed:
del self.image
else:
self._image = {"url": str(url)}
@image.deleter
def image(self):
try:
del self._image
except AttributeError:
pass
def set_image(self: E, *, url: MaybeEmpty[Any]) -> E:
"""Sets the image for the embed content.
This function returns the class instance to allow for fluent-style
@ -395,16 +426,7 @@ class Embed:
The source URL for the image. Only HTTP(S) is supported.
"""
if url is EmptyEmbed:
try:
del self._image
except AttributeError:
pass
else:
self._image = {
'url': str(url),
}
self.image = url
return self
@property
@ -420,9 +442,23 @@ class Embed:
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore
return EmbedProxy(getattr(self, "_thumbnail", {})) # type: ignore
def set_thumbnail(self: E, *, url: MaybeEmpty[str]) -> E:
@thumbnail.setter
def thumbnail(self, url: Any):
if url is EmptyEmbed:
del self.thumbnail
else:
self._thumbnail = {"url": str(url)}
@thumbnail.deleter
def thumbnail(self):
try:
del self._thumbnail
except AttributeError:
pass
def set_thumbnail(self, *, url: MaybeEmpty[Any]):
"""Sets the thumbnail for the embed content.
This function returns the class instance to allow for fluent-style
@ -437,16 +473,7 @@ class Embed:
The source URL for the thumbnail. Only HTTP(S) is supported.
"""
if url is EmptyEmbed:
try:
del self._thumbnail
except AttributeError:
pass
else:
self._thumbnail = {
'url': str(url),
}
self.thumbnail = url
return self
@property
@ -461,7 +488,7 @@ class Embed:
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_video', {})) # type: ignore
return EmbedProxy(getattr(self, "_video", {})) # type: ignore
@property
def provider(self) -> _EmbedProviderProxy:
@ -471,7 +498,7 @@ class Embed:
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_provider', {})) # type: ignore
return EmbedProxy(getattr(self, "_provider", {})) # type: ignore
@property
def author(self) -> _EmbedAuthorProxy:
@ -481,9 +508,11 @@ class Embed:
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_author', {})) # type: ignore
return EmbedProxy(getattr(self, "_author", {})) # type: ignore
def set_author(self: E, *, name: str, url: MaybeEmpty[str] = EmptyEmbed, icon_url: MaybeEmpty[str] = EmptyEmbed) -> E:
def set_author(
self: E, *, name: Any, url: MaybeEmpty[Any] = EmptyEmbed, icon_url: MaybeEmpty[Any] = EmptyEmbed
) -> E:
"""Sets the author for the embed content.
This function returns the class instance to allow for fluent-style
@ -500,14 +529,14 @@ class Embed:
"""
self._author = {
'name': str(name),
"name": str(name),
}
if url is not EmptyEmbed:
self._author['url'] = str(url)
self._author["url"] = str(url)
if icon_url is not EmptyEmbed:
self._author['icon_url'] = str(icon_url)
self._author["icon_url"] = str(icon_url)
return self
@ -528,15 +557,15 @@ class Embed:
@property
def fields(self) -> List[_EmbedFieldProxy]:
"""Union[List[:class:`EmbedProxy`], :attr:`Empty`]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents.
"""List[Union[``EmbedProxy``, :attr:`Empty`]]: 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', [])] # type: ignore
return [EmbedProxy(d) for d in getattr(self, "_fields", [])] # type: ignore
def add_field(self: E, *, name: str, value: str, inline: bool = True) -> E:
def add_field(self: E, *, name: Any, value: Any, inline: bool = True) -> E:
"""Adds a field to the embed object.
This function returns the class instance to allow for fluent-style
@ -553,9 +582,9 @@ class Embed:
"""
field = {
'inline': inline,
'name': str(name),
'value': str(value),
"inline": inline,
"name": str(name),
"value": str(value),
}
try:
@ -565,7 +594,7 @@ class Embed:
return self
def insert_field_at(self: E, index: int, *, name: str, value: str, inline: bool = True) -> E:
def insert_field_at(self: E, index: int, *, name: Any, value: Any, inline: bool = True) -> E:
"""Inserts a field before a specified index to the embed.
This function returns the class instance to allow for fluent-style
@ -586,9 +615,9 @@ class Embed:
"""
field = {
'inline': inline,
'name': str(name),
'value': str(value),
"inline": inline,
"name": str(name),
"value": str(value),
}
try:
@ -626,7 +655,7 @@ class Embed:
except (AttributeError, IndexError):
pass
def set_field_at(self: E, index: int, *, name: str, value: str, inline: bool = True) -> E:
def set_field_at(self: E, index: int, *, name: Any, value: Any, inline: bool = True) -> E:
"""Modifies a field to the embed object.
The index must point to a valid pre-existing field.
@ -654,11 +683,11 @@ class Embed:
try:
field = self._fields[index]
except (TypeError, IndexError, AttributeError):
raise IndexError('field index out of range')
raise IndexError("field index out of range")
field['name'] = str(name)
field['value'] = str(value)
field['inline'] = inline
field["name"] = str(name)
field["value"] = str(value)
field["inline"] = inline
return self
def to_dict(self) -> EmbedData:
@ -676,35 +705,35 @@ class Embed:
# deal with basic convenience wrappers
try:
colour = result.pop('colour')
colour = result.pop("colour")
except KeyError:
pass
else:
if colour:
result['color'] = colour.value
result["color"] = colour.value
try:
timestamp = result.pop('timestamp')
timestamp = result.pop("timestamp")
except KeyError:
pass
else:
if timestamp:
if timestamp.tzinfo:
result['timestamp'] = timestamp.astimezone(tz=datetime.timezone.utc).isoformat()
result["timestamp"] = timestamp.astimezone(tz=datetime.timezone.utc).isoformat()
else:
result['timestamp'] = timestamp.replace(tzinfo=datetime.timezone.utc).isoformat()
result["timestamp"] = timestamp.replace(tzinfo=datetime.timezone.utc).isoformat()
# add in the non raw attribute ones
if self.type:
result['type'] = self.type
result["type"] = self.type
if self.description:
result['description'] = self.description
result["description"] = self.description
if self.url:
result['url'] = self.url
result["url"] = self.url
if self.title:
result['title'] = self.title
result["title"] = self.title
return result # type: ignore

View File

@ -22,16 +22,26 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from .asset import Asset
from . import utils
from .partial_emoji import _EmojiTag
from __future__ import annotations
from typing import Any, Iterator, List, Optional, TYPE_CHECKING, Tuple
from .asset import Asset, AssetMixin
from .utils import SnowflakeList, snowflake_time, MISSING
from .partial_emoji import _EmojiTag, PartialEmoji
from .user import User
__all__ = (
'Emoji',
)
__all__ = ("Emoji",)
class Emoji(_EmojiTag):
if TYPE_CHECKING:
from .types.emoji import Emoji as EmojiPayload
from .guild import Guild
from .state import ConnectionState
from .abc import Snowflake
from .role import Role
from datetime import datetime
class Emoji(_EmojiTag, AssetMixin):
"""Represents a custom emoji.
Depending on the way this object was created, some of the attributes can
@ -60,6 +70,10 @@ class Emoji(_EmojiTag):
Returns the emoji rendered for discord.
.. describe:: int(x)
Returns the emoji ID.
Attributes
-----------
name: :class:`str`
@ -80,68 +94,79 @@ class Emoji(_EmojiTag):
The user that created the emoji. This can only be retrieved using :meth:`Guild.fetch_emoji` and
having the :attr:`~Permissions.manage_emojis` permission.
"""
__slots__ = ('require_colons', 'animated', 'managed', 'id', 'name', '_roles', 'guild_id',
'_state', 'user', 'available')
def __init__(self, *, guild, state, data):
self.guild_id = guild.id
self._state = state
__slots__: Tuple[str, ...] = (
"require_colons",
"animated",
"managed",
"id",
"name",
"_roles",
"guild_id",
"_state",
"user",
"available",
)
def __init__(self, *, guild: Guild, state: ConnectionState, data: EmojiPayload):
self.guild_id: int = guild.id
self._state: ConnectionState = 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.available = emoji.get('available', True)
self._roles = utils.SnowflakeList(map(int, emoji.get('roles', [])))
user = emoji.get('user')
self.user = User(state=self._state, data=user) if user else None
def _from_data(self, emoji: EmojiPayload):
self.require_colons: bool = emoji.get("require_colons", False)
self.managed: bool = emoji.get("managed", False)
self.id: int = int(emoji["id"]) # type: ignore
self.name: str = emoji["name"] # type: ignore
self.animated: bool = emoji.get("animated", False)
self.available: bool = emoji.get("available", True)
self._roles: SnowflakeList = SnowflakeList(map(int, emoji.get("roles", [])))
user = emoji.get("user")
self.user: Optional[User] = User(state=self._state, data=user) if user else None
def _iterator(self):
def _to_partial(self) -> PartialEmoji:
return PartialEmoji(name=self.name, animated=self.animated, id=self.id)
def __iter__(self) -> Iterator[Tuple[str, Any]]:
for attr in self.__slots__:
if attr[0] != '_':
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):
def __str__(self) -> str:
if self.animated:
return '<a:{0.name}:{0.id}>'.format(self)
return "<:{0.name}:{0.id}>".format(self)
return f"<a:{self.name}:{self.id}>"
return f"<:{self.name}:{self.id}>"
def __repr__(self):
return '<Emoji id={0.id} name={0.name!r} animated={0.animated} managed={0.managed}>'.format(self)
def __int__(self) -> int:
return self.id
def __eq__(self, other):
def __repr__(self) -> str:
return f"<Emoji id={self.id} name={self.name!r} animated={self.animated} managed={self.managed}>"
def __eq__(self, other: Any) -> bool:
return isinstance(other, _EmojiTag) and self.id == other.id
def __ne__(self, other):
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __hash__(self):
def __hash__(self) -> int:
return self.id >> 22
@property
def created_at(self):
def created_at(self) -> datetime:
""":class:`datetime.datetime`: Returns the emoji's creation time in UTC."""
return utils.snowflake_time(self.id)
return snowflake_time(self.id)
@property
def url(self):
""":class:`Asset`: Returns the asset of the emoji.
This is equivalent to calling :meth:`url_as` with
the default parameters (i.e. png/gif detection).
"""
return self.url_as(format=None)
def url(self) -> str:
""":class:`str`: Returns the URL of the emoji."""
fmt = "gif" if self.animated else "png"
return f"{Asset.BASE}/emojis/{self.id}.{fmt}"
@property
def roles(self):
def roles(self) -> List[Role]:
"""List[:class:`Role`]: A :class:`list` of roles that is allowed to use this emoji.
If roles is empty, the emoji is unrestricted.
@ -153,44 +178,11 @@ class Emoji(_EmojiTag):
return [role for role in guild.roles if self._roles.has(role.id)]
@property
def guild(self):
def guild(self) -> Guild:
""":class:`Guild`: The guild this emoji belongs to."""
return self._state._get_guild(self.guild_id)
def url_as(self, *, format=None, static_format="png"):
"""Returns an :class:`Asset` for the emoji's url.
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif'.
'gif' is only valid for animated emojis.
.. versionadded:: 1.6
Parameters
-----------
format: Optional[:class:`str`]
The format to attempt to convert the emojis to.
If the format is ``None``, then it is automatically
detected as either 'gif' or static_format, depending on whether the
emoji is animated or not.
static_format: Optional[:class:`str`]
Format to attempt to convert only non-animated emoji's to.
Defaults to 'png'
Raises
-------
InvalidArgument
Bad image format passed to ``format`` or ``static_format``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_emoji(self._state, self, format=format, static_format=static_format)
def is_usable(self):
def is_usable(self) -> bool:
""":class:`bool`: Whether the bot can use this emoji.
.. versionadded:: 1.3
@ -202,7 +194,7 @@ class Emoji(_EmojiTag):
emoji_roles, my_roles = self._roles, self.guild.me._roles
return any(my_roles.has(role_id) for role_id in emoji_roles)
async def delete(self, *, reason=None):
async def delete(self, *, reason: Optional[str] = None) -> None:
"""|coro|
Deletes the custom emoji.
@ -225,7 +217,9 @@ class Emoji(_EmojiTag):
await self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason)
async def edit(self, *, name=None, roles=None, reason=None):
async def edit(
self, *, name: str = MISSING, roles: List[Snowflake] = MISSING, reason: Optional[str] = None
) -> Emoji:
r"""|coro|
Edits the custom emoji.
@ -233,12 +227,15 @@ class Emoji(_EmojiTag):
You must have :attr:`~Permissions.manage_emojis` permission to
do this.
.. versionchanged:: 2.0
The newly updated emoji is returned.
Parameters
-----------
name: :class:`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.
roles: Optional[List[:class:`~discord.abc.Snowflake`]]
A list of roles that can use this emoji. An empty list can be passed to make it available to everyone.
reason: Optional[:class:`str`]
The reason for editing this emoji. Shows up on the audit log.
@ -248,9 +245,18 @@ class Emoji(_EmojiTag):
You are not allowed to edit emojis.
HTTPException
An error occurred editing the emoji.
Returns
--------
:class:`Emoji`
The newly updated emoji.
"""
name = name or self.name
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)
payload = {}
if name is not MISSING:
payload["name"] = name
if roles is not MISSING:
payload["roles"] = [role.id for role in roles]
data = await self._state.http.edit_custom_emoji(self.guild.id, self.id, payload=payload, reason=reason)
return Emoji(guild=self.guild, data=data, state=self._state)

View File

@ -24,49 +24,73 @@ DEALINGS IN THE SOFTWARE.
import types
from collections import namedtuple
from typing import Any, TYPE_CHECKING, Type, TypeVar
from typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, Type, TypeVar
__all__ = (
'Enum',
'ChannelType',
'MessageType',
'VoiceRegion',
'SpeakingState',
'VerificationLevel',
'ContentFilter',
'Status',
'DefaultAvatar',
'AuditLogAction',
'AuditLogActionCategory',
'UserFlags',
'ActivityType',
'NotificationLevel',
'TeamMembershipState',
'WebhookType',
'ExpireBehaviour',
'ExpireBehavior',
'StickerType',
"Enum",
"ChannelType",
"MessageType",
"VoiceRegion",
"SpeakingState",
"VerificationLevel",
"ContentFilter",
"Status",
"DefaultAvatar",
"AuditLogAction",
"AuditLogActionCategory",
"UserFlags",
"ActivityType",
"NotificationLevel",
"TeamMembershipState",
"WebhookType",
"ExpireBehaviour",
"ExpireBehavior",
"StickerType",
"StickerFormatType",
"InviteTarget",
"VideoQualityMode",
"ComponentType",
"ButtonStyle",
"StagePrivacyLevel",
"InteractionType",
"InteractionResponseType",
"NSFWLevel",
"ProtocolURL",
)
def _create_value_cls(name):
cls = namedtuple('_EnumValue_' + name, 'name value')
cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>'
cls.__str__ = lambda self: f'{name}.{self.name}'
def _create_value_cls(name, comparable):
cls = namedtuple("_EnumValue_" + name, "name value")
cls.__repr__ = lambda self: f"<{name}.{self.name}: {self.value!r}>"
cls.__str__ = lambda self: f"{name}.{self.name}"
if comparable:
cls.__le__ = lambda self, other: isinstance(other, self.__class__) and self.value <= other.value
cls.__ge__ = lambda self, other: isinstance(other, self.__class__) and self.value >= other.value
cls.__lt__ = lambda self, other: isinstance(other, self.__class__) and self.value < other.value
cls.__gt__ = lambda self, other: isinstance(other, self.__class__) and self.value > other.value
return cls
def _is_descriptor(obj):
return hasattr(obj, '__get__') or hasattr(obj, '__set__') or hasattr(obj, '__delete__')
return hasattr(obj, "__get__") or hasattr(obj, "__set__") or hasattr(obj, "__delete__")
class EnumMeta(type):
def __new__(cls, name, bases, attrs):
if TYPE_CHECKING:
__name__: ClassVar[str]
_enum_member_names_: ClassVar[List[str]]
_enum_member_map_: ClassVar[Dict[str, Any]]
_enum_value_map_: ClassVar[Dict[Any, Any]]
def __new__(cls, name, bases, attrs, *, comparable: bool = False):
value_mapping = {}
member_mapping = {}
member_names = []
value_cls = _create_value_cls(name)
value_cls = _create_value_cls(name, comparable)
for key, value in list(attrs.items()):
is_descriptor = _is_descriptor(value)
if key[0] == '_' and not is_descriptor:
if key[0] == "_" and not is_descriptor:
continue
# Special case classmethod to just pass through
@ -88,12 +112,12 @@ class EnumMeta(type):
member_mapping[key] = new_value
attrs[key] = new_value
attrs['_enum_value_map_'] = value_mapping
attrs['_enum_member_map_'] = member_mapping
attrs['_enum_member_names_'] = member_names
attrs['_enum_value_cls_'] = value_cls
attrs["_enum_value_map_"] = value_mapping
attrs["_enum_member_map_"] = member_mapping
attrs["_enum_member_names_"] = member_names
attrs["_enum_value_cls_"] = value_cls
actual_cls = super().__new__(cls, name, bases, attrs)
value_cls._actual_enum_cls_ = actual_cls
value_cls._actual_enum_cls_ = actual_cls # type: ignore
return actual_cls
def __iter__(cls):
@ -106,7 +130,7 @@ class EnumMeta(type):
return len(cls._enum_member_names_)
def __repr__(cls):
return f'<enum {cls.__name__}>'
return f"<enum {cls.__name__}>"
@property
def __members__(cls):
@ -122,10 +146,10 @@ class EnumMeta(type):
return cls._enum_member_map_[key]
def __setattr__(cls, name, value):
raise TypeError('Enums are immutable.')
raise TypeError("Enums are immutable.")
def __delattr__(cls, attr):
raise TypeError('Enums are immutable')
raise TypeError("Enums are immutable")
def __instancecheck__(self, instance):
# isinstance(x, Y)
@ -135,9 +159,11 @@ class EnumMeta(type):
except AttributeError:
return False
if TYPE_CHECKING:
from enum import Enum
else:
class Enum(metaclass=EnumMeta):
@classmethod
def try_value(cls, value):
@ -146,72 +172,84 @@ else:
except (KeyError, TypeError):
return value
class ChannelType(Enum):
text = 0
private = 1
voice = 2
group = 3
text = 0
private = 1
voice = 2
group = 3
category = 4
news = 5
store = 6
news = 5
store = 6
news_thread = 10
public_thread = 11
private_thread = 12
stage_voice = 13
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
premium_guild_subscription = 8
premium_guild_tier_1 = 9
premium_guild_tier_2 = 10
premium_guild_tier_3 = 11
channel_follow_add = 12
guild_stream = 13
guild_discovery_disqualified = 14
guild_discovery_requalified = 15
default = 0
recipient_add = 1
recipient_remove = 2
call = 3
channel_name_change = 4
channel_icon_change = 5
pins_add = 6
new_member = 7
premium_guild_subscription = 8
premium_guild_tier_1 = 9
premium_guild_tier_2 = 10
premium_guild_tier_3 = 11
channel_follow_add = 12
guild_stream = 13
guild_discovery_disqualified = 14
guild_discovery_requalified = 15
guild_discovery_grace_period_initial_warning = 16
guild_discovery_grace_period_final_warning = 17
guild_discovery_grace_period_final_warning = 17
thread_created = 18
reply = 19
application_command = 20
thread_starter_message = 21
guild_invite_reminder = 22
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'
south_korea = 'south-korea'
india = 'india'
europe = 'europe'
dubai = 'dubai'
vip_us_east = 'vip-us-east'
vip_us_west = 'vip-us-west'
vip_amsterdam = 'vip-amsterdam'
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"
south_korea = "south-korea"
india = "india"
europe = "europe"
dubai = "dubai"
vip_us_east = "vip-us-east"
vip_us_west = "vip-us-west"
vip_amsterdam = "vip-amsterdam"
def __str__(self):
return self.value
class SpeakingState(Enum):
none = 0
voice = 1
none = 0
voice = 1
soundshare = 2
priority = 4
priority = 4
def __str__(self):
return self.name
@ -219,59 +257,64 @@ class SpeakingState(Enum):
def __int__(self):
return self.value
class VerificationLevel(Enum):
none = 0
low = 1
medium = 2
high = 3
table_flip = 3
extreme = 4
double_table_flip = 4
very_high = 4
class VerificationLevel(Enum, comparable=True):
none = 0
low = 1
medium = 2
high = 3
highest = 4
def __str__(self):
return self.name
class ContentFilter(Enum):
disabled = 0
no_role = 1
class ContentFilter(Enum, comparable=True):
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'
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
grey = 1
gray = 1
green = 2
orange = 3
red = 4
def __str__(self):
return self.name
class NotificationLevel(Enum):
all_messages = 0
class NotificationLevel(Enum, comparable=True):
all_messages = 0
only_mentions = 1
class AuditLogActionCategory(Enum):
create = 1
delete = 2
update = 3
class AuditLogAction(Enum):
# fmt: off
guild_update = 1
channel_create = 10
channel_update = 11
@ -307,71 +350,101 @@ class AuditLogAction(Enum):
integration_create = 80
integration_update = 81
integration_delete = 82
stage_instance_create = 83
stage_instance_update = 84
stage_instance_delete = 85
sticker_create = 90
sticker_update = 91
sticker_delete = 92
thread_create = 110
thread_update = 111
thread_delete = 112
# fmt: on
@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.member_move: None,
AuditLogAction.member_disconnect: None,
AuditLogAction.bot_add: None,
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,
AuditLogAction.message_bulk_delete: AuditLogActionCategory.delete,
AuditLogAction.message_pin: None,
AuditLogAction.message_unpin: None,
AuditLogAction.integration_create: AuditLogActionCategory.create,
AuditLogAction.integration_update: AuditLogActionCategory.update,
AuditLogAction.integration_delete: AuditLogActionCategory.delete,
def category(self) -> Optional[AuditLogActionCategory]:
# fmt: off
lookup: Dict[AuditLogAction, Optional[AuditLogActionCategory]] = {
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.member_move: None,
AuditLogAction.member_disconnect: None,
AuditLogAction.bot_add: None,
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,
AuditLogAction.message_bulk_delete: AuditLogActionCategory.delete,
AuditLogAction.message_pin: None,
AuditLogAction.message_unpin: None,
AuditLogAction.integration_create: AuditLogActionCategory.create,
AuditLogAction.integration_update: AuditLogActionCategory.update,
AuditLogAction.integration_delete: AuditLogActionCategory.delete,
AuditLogAction.stage_instance_create: AuditLogActionCategory.create,
AuditLogAction.stage_instance_update: AuditLogActionCategory.update,
AuditLogAction.stage_instance_delete: AuditLogActionCategory.delete,
AuditLogAction.sticker_create: AuditLogActionCategory.create,
AuditLogAction.sticker_update: AuditLogActionCategory.update,
AuditLogAction.sticker_delete: AuditLogActionCategory.delete,
AuditLogAction.thread_create: AuditLogActionCategory.create,
AuditLogAction.thread_update: AuditLogActionCategory.update,
AuditLogAction.thread_delete: AuditLogActionCategory.delete,
}
# fmt: on
return lookup[self]
@property
def target_type(self):
def target_type(self) -> Optional[str]:
v = self.value
if v == -1:
return 'all'
return "all"
elif v < 10:
return 'guild'
return "guild"
elif v < 20:
return 'channel'
return "channel"
elif v < 30:
return 'user'
return "user"
elif v < 40:
return 'role'
return "role"
elif v < 50:
return 'invite'
return "invite"
elif v < 60:
return 'webhook'
return "webhook"
elif v < 70:
return 'emoji'
return "emoji"
elif v == 73:
return "channel"
elif v < 80:
return 'message'
return "message"
elif v < 83:
return "integration"
elif v < 90:
return 'integration'
return "stage_instance"
elif v < 93:
return "sticker"
elif v < 113:
return "thread"
class UserFlags(Enum):
staff = 1
@ -390,6 +463,8 @@ class UserFlags(Enum):
bug_hunter_level_2 = 16384
verified_bot = 65536
verified_bot_developer = 131072
discord_certified_moderator = 262144
class ActivityType(Enum):
unknown = -1
@ -403,36 +478,198 @@ class ActivityType(Enum):
def __int__(self):
return self.value
class TeamMembershipState(Enum):
invited = 1
accepted = 2
class WebhookType(Enum):
incoming = 1
channel_follower = 2
application = 3
class ExpireBehaviour(Enum):
remove_role = 0
kick = 1
ExpireBehavior = ExpireBehaviour
class StickerType(Enum):
standard = 1
guild = 2
class StickerFormatType(Enum):
png = 1
apng = 2
lottie = 3
@property
def file_extension(self) -> str:
# fmt: off
lookup: Dict[StickerFormatType, str] = {
StickerFormatType.png: 'png',
StickerFormatType.apng: 'png',
StickerFormatType.lottie: 'json',
}
# fmt: on
return lookup[self]
class InviteTarget(Enum):
unknown = 0
stream = 1
embedded_application = 2
class InteractionType(Enum):
ping = 1
application_command = 2
component = 3
application_command_autocomplete = 4
class InteractionResponseType(Enum):
pong = 1
# ack = 2 (deprecated)
# channel_message = 3 (deprecated)
channel_message = 4 # (with source)
deferred_channel_message = 5 # (with source)
deferred_message_update = 6 # for components
message_update = 7 # for components
application_command_autocomplete_result = 8
class VideoQualityMode(Enum):
auto = 1
full = 2
def __int__(self):
return self.value
class ComponentType(Enum):
action_row = 1
button = 2
select = 3
def __int__(self):
return self.value
class ButtonStyle(Enum):
primary = 1
secondary = 2
success = 3
danger = 4
link = 5
# Aliases
blurple = 1
grey = 2
gray = 2
green = 3
red = 4
url = 5
def __int__(self):
return self.value
class StagePrivacyLevel(Enum):
public = 1
closed = 2
guild_only = 2
class NSFWLevel(Enum, comparable=True):
default = 0
explicit = 1
safe = 2
age_restricted = 3
class ProtocolURL(Enum):
# General
home = "discord://-/channels/@me/"
nitro = "discord://-/store"
apps = "discord://-/apps" # Breaks the client on windows (Shows download links for different OS)
guild_discovery = "discord://-/guild-discovery"
guild_create = "discord://-/guilds/create"
guild_invite = "discord://-/invite/{invite_code}"
# Settings
account_settings = "discord://-/settings/account"
profile_settings = "discord://-/settings/profile-customization"
privacy_settings = "discord://-/settings/privacy-and-safety"
safety_settings = "discord://-/settings/privacy-and-safety" # Alias
authorized_apps_settings = "discord://-/settings/authorized-apps"
connections_settings = "discord://-/settings/connections"
nitro_settings = "discord://-/settings/premium" # Same as store, but inside of settings
guild_premium_subscription = "discord://-/settings/premium-guild-subscription"
subscription_settings = "discord://-/settings/subscriptions"
gift_inventory_settings = "discord://-/settings/inventory"
billing_settings = "discord://-/settings/billing"
appearance_settings = "discord://-/settings/appearance"
accessibility_settings = "discord://-/settings/accessibility"
voice_video_settings = "discord://-/settings/voice"
text_images_settings = "discord://-/settings/text"
notifications_settings = "discord://-/settings/notifications"
keybinds_settings = "discord://-/settings/keybinds"
language_settings = "discord://-/settings/locale"
windows_settings = "discord://-/settings/windows" # Doesnt work if used on wrong platform
linux_settings = "discord://-/settings/linux" # Doesnt work if used on wrong platform
streamer_mode_settings = "discord://-/settings/streamer-mode"
advanced_settings = "discord://-/settings/advanced"
activity_status_settings = "discord://-/settings/activity-status"
game_overlay_settings = "discord://-/settings/overlay"
hypesquad_settings = "discord://-/settings/hypesquad-online"
changelogs = "discord://-/settings/changelogs"
# Doesn't work if you don't have it actually activated. Just blank screen.
experiments = "discord://-/settings/experiments"
developer_options = "discord://-/settings/developer-options" # Same as experiments
hotspot_options = "discord://-/settings/hotspot-options" # Same as experiments
# Users, Guilds, and DMs
user_profile = "discord://-/users/{user_id}"
dm_channel = "discord://-/channels/@me/{channel_id}"
dm_message = "discord://-/channels/@me/{channel_id}/{message_id}"
guild_channel = "discord://-/channels/{guild_id}/{channel_id}"
guild_message = "discord://-/channels/{guild_id}/{channel_id}/{message_id}"
guild_membership_screening = "discord://-/member-verification/{guild_id}"
# Library
games_library = "discord://-/library"
library_settings = "discord://-/library/settings"
def __str__(self) -> str:
return self.value
def format(self, **kwargs: Any) -> str:
return self.value.format(**kwargs)
T = TypeVar("T")
T = TypeVar('T')
def create_unknown_value(cls: Type[T], val: Any) -> T:
value_cls = cls._enum_value_cls_ # type: ignore
name = f'unknown_{val}'
value_cls = cls._enum_value_cls_ # type: ignore
name = f"unknown_{val}"
return value_cls(name=name, value=val)
def try_enum(cls: Type[T], val: Any) -> T:
"""A function that tries to turn the value into enum ``cls``.
@ -440,6 +677,6 @@ def try_enum(cls: Type[T], val: Any) -> T:
"""
try:
return cls._enum_value_map_[val] # type: ignore
return cls._enum_value_map_[val] # type: ignore
except (KeyError, TypeError, AttributeError):
return create_unknown_value(cls, val)

View File

@ -22,67 +22,91 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Dict, List, Optional, TYPE_CHECKING, Any, Tuple, Union
if TYPE_CHECKING:
from aiohttp import ClientResponse, ClientWebSocketResponse
try:
from requests import Response
_ResponseType = Union[ClientResponse, Response]
except ModuleNotFoundError:
_ResponseType = ClientResponse
from .interactions import Interaction
__all__ = (
'DiscordException',
'ClientException',
'NoMoreItems',
'GatewayNotFound',
'HTTPException',
'Forbidden',
'NotFound',
'DiscordServerError',
'InvalidData',
'InvalidArgument',
'LoginFailure',
'ConnectionClosed',
'PrivilegedIntentsRequired',
"DiscordException",
"ClientException",
"NoMoreItems",
"GatewayNotFound",
"HTTPException",
"Forbidden",
"NotFound",
"DiscordServerError",
"InvalidData",
"InvalidArgument",
"LoginFailure",
"ConnectionClosed",
"PrivilegedIntentsRequired",
"InteractionResponded",
)
class DiscordException(Exception):
"""Base exception class for discord.py
Ideally speaking, this could be caught to handle any exceptions thrown from this library.
Ideally speaking, this could be caught to handle any exceptions raised from this library.
"""
pass
class ClientException(DiscordException):
"""Exception that's thrown when an operation in the :class:`Client` fails.
"""Exception that's raised 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."""
"""Exception that is raised 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."""
"""An exception that is raised when the gateway for Discord could not be found"""
def __init__(self):
message = 'The gateway to connect to discord was not found.'
message = "The gateway to connect to discord was not found."
super().__init__(message)
def flatten_error_dict(d, key=''):
items = []
def _flatten_error_dict(d: Dict[str, Any], key: str = "") -> Dict[str, str]:
items: List[Tuple[str, str]] = []
for k, v in d.items():
new_key = key + '.' + k if key else k
new_key = key + "." + k if key else k
if isinstance(v, dict):
try:
_errors = v['_errors']
_errors: List[Dict[str, Any]] = v["_errors"]
except KeyError:
items.extend(flatten_error_dict(v, new_key).items())
items.extend(_flatten_error_dict(v, new_key).items())
else:
items.append((new_key, ' '.join(x.get('message', '') for x in _errors)))
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.
"""Exception that's raised when an HTTP request operation fails.
Attributes
------------
@ -99,77 +123,92 @@ class HTTPException(DiscordException):
The Discord specific error code for the failure.
"""
def __init__(self, response, message):
self.response = response
self.status = response.status
def __init__(self, response: _ResponseType, message: Optional[Union[str, Dict[str, Any]]]):
self.response: _ResponseType = response
self.status: int = response.status # type: ignore
self.code: int
self.text: str
if isinstance(message, dict):
self.code = message.get('code', 0)
base = message.get('message', '')
errors = message.get('errors')
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
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.text = message or ""
self.code = 0
fmt = '{0.status} {0.reason} (error code: {1})'
fmt = "{0.status} {0.reason} (error code: {1})"
if len(self.text):
fmt += ': {2}'
fmt += ": {2}"
super().__init__(fmt.format(self.response, self.code, self.text))
class Forbidden(HTTPException):
"""Exception that's thrown for when status code 403 occurs.
"""Exception that's raised for when status code 403 occurs.
Subclass of :exc:`HTTPException`
"""
pass
class NotFound(HTTPException):
"""Exception that's thrown for when status code 404 occurs.
"""Exception that's raised for when status code 404 occurs.
Subclass of :exc:`HTTPException`
"""
pass
class DiscordServerError(HTTPException):
"""Exception that's thrown for when a 500 range status code occurs.
"""Exception that's raised for when a 500 range status code occurs.
Subclass of :exc:`HTTPException`.
.. versionadded:: 1.5
"""
pass
class InvalidData(ClientException):
"""Exception that's raised when the library encounters unknown
or invalid data from Discord.
"""
pass
class InvalidArgument(ClientException):
"""Exception that's thrown when an argument to a function
"""Exception that's raised 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 inherited from :exc:`ClientException` and thus
:exc:`DiscordException`.
"""
pass
class LoginFailure(ClientException):
"""Exception that's thrown when the :meth:`Client.login` function
"""Exception that's raised 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
"""Exception that's raised when the gateway connection is
closed for reasons that could not be handled internally.
Attributes
@ -181,17 +220,19 @@ class ConnectionClosed(ClientException):
shard_id: Optional[:class:`int`]
The shard ID that got closed if applicable.
"""
def __init__(self, socket, *, shard_id, code=None):
def __init__(self, socket: ClientWebSocketResponse, *, shard_id: Optional[int], code: Optional[int] = None):
# This exception is just the same exception except
# reconfigured to subclass ClientException for users
self.code = code or socket.close_code
self.code: int = code or socket.close_code or -1
# aiohttp doesn't seem to consistently provide close reason
self.reason = ''
self.shard_id = shard_id
super().__init__(f'Shard ID {self.shard_id} WebSocket closed with {self.code}')
self.reason: str = ""
self.shard_id: Optional[int] = shard_id
super().__init__(f"Shard ID {self.shard_id} WebSocket closed with {self.code}")
class PrivilegedIntentsRequired(ClientException):
"""Exception that's thrown when the gateway is requesting privileged intents
"""Exception that's raised when the gateway is requesting privileged intents
but they're not ticked in the developer page yet.
Go to https://discord.com/developers/applications/ and enable the intents
@ -206,10 +247,31 @@ class PrivilegedIntentsRequired(ClientException):
The shard ID that got closed if applicable.
"""
def __init__(self, shard_id):
self.shard_id = shard_id
msg = 'Shard ID %s is requesting privileged intents that have not been explicitly enabled in the ' \
'developer portal. It is recommended to go to https://discord.com/developers/applications/ ' \
'and explicitly enable the privileged intents within your application\'s page. If this is not ' \
'possible, then consider disabling the privileged intents instead.'
def __init__(self, shard_id: Optional[int]):
self.shard_id: Optional[int] = shard_id
msg = (
"Shard ID %s is requesting privileged intents that have not been explicitly enabled in the "
"developer portal. It is recommended to go to https://discord.com/developers/applications/ "
"and explicitly enable the privileged intents within your application's page. If this is not "
"possible, then consider disabling the privileged intents instead."
)
super().__init__(msg % shard_id)
class InteractionResponded(ClientException):
"""Exception that's raised when sending another interaction response using
:class:`InteractionResponse` when one has already been done before.
An interaction can only respond once.
.. versionadded:: 2.0
Attributes
-----------
interaction: :class:`Interaction`
The interaction that's already been responded to.
"""
def __init__(self, interaction: Interaction):
self.interaction: Interaction = interaction
super().__init__("This interaction has already been responded to before")

View File

@ -16,3 +16,4 @@ from .help import *
from .converter import *
from .cooldowns import *
from .cog import *
from .flags import *

View File

@ -22,6 +22,28 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from typing import Any, Callable, Coroutine, TYPE_CHECKING, TypeVar, Union
if TYPE_CHECKING:
from .context import Context
from .cog import Cog
from .errors import CommandError
T = TypeVar("T")
Coro = Coroutine[Any, Any, T]
MaybeCoro = Union[T, Coro[T]]
CoroFunc = Callable[..., Coro[Any]]
Check = Union[Callable[["Cog", "Context[Any]"], MaybeCoro[bool]], Callable[["Context[Any]"], MaybeCoro[bool]]]
Hook = Union[Callable[["Cog", "Context[Any]"], Coro[Any]], Callable[["Context[Any]"], Coro[Any]]]
Error = Union[
Callable[["Cog", "Context[Any]", "CommandError"], Coro[Any]], Callable[["Context[Any]", "CommandError"], Coro[Any]]
]
# This is merely a tag type to avoid circular import issues.
# Yes, this is a terrible solution but ultimately it is the only solution.
class _BaseCommand:

File diff suppressed because it is too large Load Diff

View File

@ -21,16 +21,31 @@ 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 __future__ import annotations
import inspect
import copy
import discord.utils
from typing import Any, Callable, ClassVar, Dict, Generator, List, Optional, TYPE_CHECKING, Tuple, TypeVar, Type
from ._types import _BaseCommand
if TYPE_CHECKING:
from .bot import BotBase
from .context import Context
from .core import Command
__all__ = (
'CogMeta',
'Cog',
"CogMeta",
"Cog",
)
CogT = TypeVar("CogT", bound="Cog")
FuncT = TypeVar("FuncT", bound=Callable[..., Any])
MISSING: Any = discord.utils.MISSING
class CogMeta(type):
"""A metaclass for defining a cog.
@ -91,19 +106,24 @@ class CogMeta(type):
pass # hidden -> False
"""
def __new__(cls, *args, **kwargs):
name, bases, attrs = args
attrs['__cog_name__'] = kwargs.pop('name', name)
attrs['__cog_settings__'] = kwargs.pop('command_attrs', {})
__cog_name__: str
__cog_settings__: Dict[str, Any]
__cog_commands__: List[Command]
__cog_listeners__: List[Tuple[str, str]]
description = kwargs.pop('description', None)
def __new__(cls: Type[CogMeta], *args: Any, **kwargs: Any) -> CogMeta:
name, bases, attrs = args
attrs["__cog_name__"] = kwargs.pop("name", name)
attrs["__cog_settings__"] = kwargs.pop("command_attrs", {})
description = kwargs.pop("description", None)
if description is None:
description = inspect.cleandoc(attrs.get('__doc__', ''))
attrs['__cog_description__'] = description
description = inspect.cleandoc(attrs.get("__doc__", ""))
attrs["__cog_description__"] = description
commands = {}
listeners = {}
no_bot_cog = 'Commands or listeners must not start with cog_ or bot_ (in method {0.__name__}.{1})'
no_bot_cog = "Commands or listeners must not start with cog_ or bot_ (in method {0.__name__}.{1})"
new_cls = super().__new__(cls, name, bases, attrs, **kwargs)
for base in reversed(new_cls.__mro__):
@ -118,21 +138,21 @@ class CogMeta(type):
value = value.__func__
if isinstance(value, _BaseCommand):
if is_static_method:
raise TypeError(f'Command in method {base}.{elem!r} must not be staticmethod.')
if elem.startswith(('cog_', 'bot_')):
raise TypeError(f"Command in method {base}.{elem!r} must not be staticmethod.")
if elem.startswith(("cog_", "bot_")):
raise TypeError(no_bot_cog.format(base, elem))
commands[elem] = value
elif inspect.iscoroutinefunction(value):
try:
getattr(value, '__cog_listener__')
getattr(value, "__cog_listener__")
except AttributeError:
continue
else:
if elem.startswith(('cog_', 'bot_')):
if elem.startswith(("cog_", "bot_")):
raise TypeError(no_bot_cog.format(base, elem))
listeners[elem] = value
new_cls.__cog_commands__ = list(commands.values()) # this will be copied in Cog.__new__
new_cls.__cog_commands__ = list(commands.values()) # this will be copied in Cog.__new__
listeners_as_list = []
for listener in listeners.values():
@ -144,17 +164,19 @@ class CogMeta(type):
new_cls.__cog_listeners__ = listeners_as_list
return new_cls
def __init__(self, *args, **kwargs):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args)
@classmethod
def qualified_name(cls):
def qualified_name(cls) -> str:
return cls.__cog_name__
def _cog_special_method(func):
def _cog_special_method(func: FuncT) -> FuncT:
func.__cog_special_method__ = None
return func
class Cog(metaclass=CogMeta):
"""The base class that all cogs must inherit from.
@ -166,7 +188,12 @@ class Cog(metaclass=CogMeta):
are equally valid here.
"""
def __new__(cls, *args, **kwargs):
__cog_name__: ClassVar[str]
__cog_settings__: ClassVar[Dict[str, Any]]
__cog_commands__: ClassVar[List[Command]]
__cog_listeners__: ClassVar[List[Tuple[str, str]]]
def __new__(cls: Type[CogT], *args: Any, **kwargs: Any) -> CogT:
# For issue 426, we need to store a copy of the command objects
# since we modify them to inject `self` to them.
# To do this, we need to interfere with the Cog creation process.
@ -174,12 +201,10 @@ class Cog(metaclass=CogMeta):
cmd_attrs = cls.__cog_settings__
# Either update the command with the cog provided defaults or copy it.
self.__cog_commands__ = tuple(c._update_copy(cmd_attrs) for c in cls.__cog_commands__)
# r.e type ignore, type-checker complains about overriding a ClassVar
self.__cog_commands__ = tuple(c._update_copy(cmd_attrs) for c in cls.__cog_commands__) # type: ignore
lookup = {
cmd.qualified_name: cmd
for cmd in self.__cog_commands__
}
lookup = {cmd.qualified_name: cmd for cmd in self.__cog_commands__}
# Update the Command instances dynamically as well
for command in self.__cog_commands__:
@ -187,15 +212,15 @@ class Cog(metaclass=CogMeta):
parent = command.parent
if parent is not None:
# Get the latest parent reference
parent = lookup[parent.qualified_name]
parent = lookup[parent.qualified_name] # type: ignore
# Update our parent's reference to our self
parent.remove_command(command.name)
parent.add_command(command)
parent.remove_command(command.name) # type: ignore
parent.add_command(command) # type: ignore
return self
def get_commands(self):
def get_commands(self) -> List[Command]:
r"""
Returns
--------
@ -210,20 +235,20 @@ class Cog(metaclass=CogMeta):
return [c for c in self.__cog_commands__ if c.parent is None]
@property
def qualified_name(self):
def qualified_name(self) -> str:
""":class:`str`: Returns the cog's specified name, not the class name."""
return self.__cog_name__
@property
def description(self):
def description(self) -> str:
""":class:`str`: Returns the cog's description, typically the cleaned docstring."""
return self.__cog_description__
@description.setter
def description(self, description):
def description(self, description: str) -> None:
self.__cog_description__ = description
def walk_commands(self):
def walk_commands(self) -> Generator[Command, None, None]:
"""An iterator that recursively walks through this cog's commands and subcommands.
Yields
@ -232,13 +257,14 @@ class Cog(metaclass=CogMeta):
A command or group from the cog.
"""
from .core import GroupMixin
for command in self.__cog_commands__:
if command.parent is None:
yield command
if isinstance(command, GroupMixin):
yield from command.walk_commands()
def get_listeners(self):
def get_listeners(self) -> List[Tuple[str, Callable[..., Any]]]:
"""Returns a :class:`list` of (name, function) listener pairs that are defined in this cog.
Returns
@ -249,12 +275,12 @@ class Cog(metaclass=CogMeta):
return [(name, getattr(self, method_name)) for name, method_name in self.__cog_listeners__]
@classmethod
def _get_overridden_method(cls, method):
def _get_overridden_method(cls, method: FuncT) -> Optional[FuncT]:
"""Return None if the method is not overridden. Otherwise returns the overridden method."""
return getattr(method.__func__, '__cog_special_method__', method)
return getattr(method.__func__, "__cog_special_method__", method)
@classmethod
def listener(cls, name=None):
def listener(cls, name: str = MISSING) -> Callable[[FuncT], FuncT]:
"""A decorator that marks a function as a listener.
This is the cog equivalent of :meth:`.Bot.listen`.
@ -272,15 +298,15 @@ class Cog(metaclass=CogMeta):
the name.
"""
if name is not None and not isinstance(name, str):
raise TypeError(f'Cog.listener expected str but received {name.__class__.__name__!r} instead.')
if name is not MISSING and not isinstance(name, str):
raise TypeError(f"Cog.listener expected str but received {name.__class__.__name__!r} instead.")
def decorator(func):
def decorator(func: FuncT) -> FuncT:
actual = func
if isinstance(actual, staticmethod):
actual = actual.__func__
if not inspect.iscoroutinefunction(actual):
raise TypeError('Listener function must be a coroutine function.')
raise TypeError("Listener function must be a coroutine function.")
actual.__cog_listener__ = True
to_assign = name or actual.__name__
try:
@ -292,17 +318,18 @@ class Cog(metaclass=CogMeta):
# to pick it up but the metaclass unfurls the function and
# thus the assignments need to be on the actual function
return func
return decorator
def has_error_handler(self):
def has_error_handler(self) -> bool:
""":class:`bool`: Checks whether the cog has an error handler.
.. versionadded:: 1.7
"""
return not hasattr(self.cog_command_error.__func__, '__cog_special_method__')
return not hasattr(self.cog_command_error.__func__, "__cog_special_method__")
@_cog_special_method
def cog_unload(self):
def cog_unload(self) -> None:
"""A special method that is called when the cog gets removed.
This function **cannot** be a coroutine. It must be a regular
@ -313,7 +340,7 @@ class Cog(metaclass=CogMeta):
pass
@_cog_special_method
def bot_check_once(self, ctx):
def bot_check_once(self, ctx: Context) -> bool:
"""A special method that registers as a :meth:`.Bot.check_once`
check.
@ -323,7 +350,7 @@ class Cog(metaclass=CogMeta):
return True
@_cog_special_method
def bot_check(self, ctx):
def bot_check(self, ctx: Context) -> bool:
"""A special method that registers as a :meth:`.Bot.check`
check.
@ -333,8 +360,8 @@ class Cog(metaclass=CogMeta):
return True
@_cog_special_method
def cog_check(self, ctx):
"""A special method that registers as a :func:`commands.check`
def cog_check(self, ctx: Context) -> bool:
"""A special method that registers as a :func:`~discord.ext.commands.check`
for every command and subcommand in this cog.
This function **can** be a coroutine and must take a sole parameter,
@ -343,7 +370,7 @@ class Cog(metaclass=CogMeta):
return True
@_cog_special_method
async def cog_command_error(self, ctx, error):
async def cog_command_error(self, ctx: Context, error: Exception) -> None:
"""A special method that is called whenever an error
is dispatched inside this cog.
@ -362,7 +389,7 @@ class Cog(metaclass=CogMeta):
pass
@_cog_special_method
async def cog_before_invoke(self, ctx):
async def cog_before_invoke(self, ctx: Context) -> None:
"""A special method that acts as a cog local pre-invoke hook.
This is similar to :meth:`.Command.before_invoke`.
@ -377,7 +404,7 @@ class Cog(metaclass=CogMeta):
pass
@_cog_special_method
async def cog_after_invoke(self, ctx):
async def cog_after_invoke(self, ctx: Context) -> None:
"""A special method that acts as a cog local post-invoke hook.
This is similar to :meth:`.Command.after_invoke`.
@ -391,7 +418,7 @@ class Cog(metaclass=CogMeta):
"""
pass
def _inject(self, bot):
def _inject(self: CogT, bot: BotBase) -> CogT:
cls = self.__class__
# realistically, the only thing that can cause loading errors
@ -426,7 +453,7 @@ class Cog(metaclass=CogMeta):
return self
def _eject(self, bot):
def _eject(self, bot: BotBase) -> None:
cls = self.__class__
try:

View File

@ -21,15 +21,54 @@ 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 __future__ import annotations
import asyncio
import inspect
import re
from datetime import timedelta
from typing import Any, Dict, Generic, List, Literal, NoReturn, Optional, TYPE_CHECKING, TypeVar, Union, overload
import discord.abc
import discord.utils
__all__ = (
'Context',
)
from discord.message import Message
from discord import Permissions
class Context(discord.abc.Messageable):
if TYPE_CHECKING:
from typing_extensions import ParamSpec
from discord.abc import MessageableChannel
from discord.guild import Guild
from discord.member import Member
from discord.state import ConnectionState
from discord.user import ClientUser, User
from discord.webhook import WebhookMessage
from discord.interactions import Interaction
from discord.voice_client import VoiceProtocol
from .bot import Bot, AutoShardedBot
from .cog import Cog
from .core import Command
from .help import HelpCommand
from .view import StringView
__all__ = ("Context",)
MISSING: Any = discord.utils.MISSING
T = TypeVar("T")
BotT = TypeVar("BotT", bound="Union[Bot, AutoShardedBot]")
CogT = TypeVar("CogT", bound="Cog")
if TYPE_CHECKING:
P = ParamSpec("P")
else:
P = TypeVar("P")
class Context(discord.abc.Messageable, Generic[BotT]):
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
@ -46,17 +85,22 @@ class Context(discord.abc.Messageable):
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
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`
:func:`.on_command_error` event then this dict could be incomplete.
current_parameter: Optional[:class:`inspect.Parameter`]
The parameter that is currently being inspected and converted.
This is only of use for within converters.
.. versionadded:: 2.0
prefix: Optional[:class:`str`]
The prefix that was used to invoke the command.
command: :class:`Command`
command: Optional[:class:`Command`]
The command that is being invoked currently.
invoked_with: :class:`str`
invoked_with: Optional[:class:`str`]
The command name that triggered this invocation. Useful for finding out
which alias called the command.
invoked_parents: List[:class:`str`]
@ -67,7 +111,7 @@ class Context(discord.abc.Messageable):
.. versionadded:: 1.7
invoked_subcommand: :class:`Command`
invoked_subcommand: Optional[:class:`Command`]
The subcommand that was invoked.
If no valid subcommand was invoked then this is equal to ``None``.
subcommand_passed: Optional[:class:`str`]
@ -79,23 +123,43 @@ class Context(discord.abc.Messageable):
A boolean that indicates if the command failed to be parsed, checked,
or invoked.
"""
interaction: Optional[Interaction] = None
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_parents = attrs.pop('invoked_parents', [])
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
def __init__(
self,
*,
message: Message,
bot: BotT,
view: StringView,
args: List[Any] = MISSING,
kwargs: Dict[str, Any] = MISSING,
prefix: Optional[str] = None,
command: Optional[Command] = None,
invoked_with: Optional[str] = None,
invoked_parents: List[str] = MISSING,
invoked_subcommand: Optional[Command] = None,
subcommand_passed: Optional[str] = None,
command_failed: bool = False,
current_parameter: Optional[inspect.Parameter] = None,
):
self.message: Message = message
self.bot: BotT = bot
self.args: List[Any] = args or []
self.kwargs: Dict[str, Any] = kwargs or {}
self.prefix: Optional[str] = prefix
self.command: Optional[Command] = command
self.view: StringView = view
self.invoked_with: Optional[str] = invoked_with
self.invoked_parents: List[str] = invoked_parents or []
self.invoked_subcommand: Optional[Command] = invoked_subcommand
self.subcommand_passed: Optional[str] = subcommand_passed
self.command_failed: bool = command_failed
self.current_parameter: Optional[inspect.Parameter] = current_parameter
self._ignored_params: List[inspect.Parameter] = []
self._typing_task: Optional[asyncio.Task[NoReturn]] = None
self._state: ConnectionState = self.message._state
async def invoke(self, command, /, *args, **kwargs):
async def invoke(self, command: Command[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs) -> T:
r"""|coro|
Calls a command with the arguments given.
@ -117,7 +181,7 @@ class Context(discord.abc.Messageable):
command: :class:`.Command`
The command that is going to be called.
\*args
The arguments to to use.
The arguments to use.
\*\*kwargs
The keyword arguments to use.
@ -126,17 +190,9 @@ class Context(discord.abc.Messageable):
TypeError
The command argument to invoke is missing.
"""
arguments = []
if command.cog is not None:
arguments.append(command.cog)
return await command(self, *args, **kwargs)
arguments.append(self)
arguments.extend(args)
ret = await command.callback(*arguments, **kwargs)
return ret
async def reinvoke(self, *, call_hooks=False, restart=True):
async def reinvoke(self, *, call_hooks: bool = False, restart: bool = True) -> None:
"""|coro|
Calls the command again.
@ -169,7 +225,7 @@ class Context(discord.abc.Messageable):
cmd = self.command
view = self.view
if cmd is None:
raise ValueError('This context is not valid.')
raise ValueError("This context is not valid.")
# some state to revert to when we're done
index, previous = view.index, view.previous
@ -180,10 +236,10 @@ class Context(discord.abc.Messageable):
if restart:
to_call = cmd.root_parent or cmd
view.index = len(self.prefix)
view.index = len(self.prefix or "")
view.previous = 0
self.invoked_parents = []
self.invoked_with = view.get_word() # advance to get the root command
self.invoked_with = view.get_word() # advance to get the root command
else:
to_call = cmd
@ -199,15 +255,32 @@ class Context(discord.abc.Messageable):
self.subcommand_passed = subcommand_passed
@property
def valid(self):
def valid(self) -> bool:
""":class:`bool`: 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):
async def _get_channel(self) -> discord.abc.Messageable:
return self.channel
@property
def cog(self):
def clean_prefix(self) -> str:
""":class:`str`: The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``.
.. versionadded:: 2.0
"""
if self.prefix is None:
return ""
user = self.me
# 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.
pattern = re.compile(r"<@!?%s>" % user.id)
return pattern.sub("@%s" % user.display_name.replace("\\", r"\\"), self.prefix)
@property
def cog(self) -> Optional[Cog]:
"""Optional[:class:`.Cog`]: Returns the cog associated with this context's command. None if it does not exist."""
if self.command is None:
@ -215,38 +288,46 @@ class Context(discord.abc.Messageable):
return self.command.cog
@discord.utils.cached_property
def guild(self):
def guild(self) -> Optional[Guild]:
"""Optional[:class:`.Guild`]: Returns the guild associated with this context's command. None if not available."""
return self.message.guild
@discord.utils.cached_property
def channel(self):
def channel(self) -> MessageableChannel:
"""Union[:class:`.abc.Messageable`]: 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):
def author(self) -> Union[User, Member]:
"""Union[:class:`~discord.User`, :class:`.Member`]:
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):
def me(self) -> Union[Member, ClientUser]:
"""Union[:class:`.Member`, :class:`.ClientUser`]:
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
# bot.user will never be None at this point.
return self.guild.me if self.guild is not None else self.bot.user # type: ignore
@property
def voice_client(self):
def voice_client(self) -> Optional[VoiceProtocol]:
r"""Optional[:class:`.VoiceProtocol`]: A shortcut to :attr:`.Guild.voice_client`\, if applicable."""
g = self.guild
return g.voice_client if g else None
async def send_help(self, *args):
def author_permissions(self) -> Permissions:
"""Returns the author permissions in the given channel.
.. versionadded:: 2.0
"""
return self.channel.permissions_for(self.author)
async def send_help(self, *args: Any) -> Any:
"""send_help(entity=<bot>)
|coro|
@ -298,12 +379,12 @@ class Context(discord.abc.Messageable):
return None
entity = args[0]
if entity is None:
return None
if isinstance(entity, str):
entity = bot.get_cog(entity) or bot.get_command(entity)
if entity is None:
return None
try:
entity.qualified_name
except AttributeError:
@ -313,7 +394,7 @@ class Context(discord.abc.Messageable):
await cmd.prepare_help_command(self, entity.qualified_name)
try:
if hasattr(entity, '__cog_commands__'):
if hasattr(entity, "__cog_commands__"):
injected = wrap_callback(cmd.send_cog_help)
return await injected(entity)
elif isinstance(entity, Group):
@ -327,6 +408,128 @@ class Context(discord.abc.Messageable):
except CommandError as e:
await cmd.on_help_command_error(self, e)
@discord.utils.copy_doc(discord.Message.reply)
async def reply(self, content=None, **kwargs):
return await self.message.reply(content, **kwargs)
@overload
async def send(
self,
content: Optional[str] = None,
return_message: Literal[False] = False,
ephemeral: bool = False,
**kwargs: Any,
) -> Optional[Union[Message, WebhookMessage]]:
...
@overload
async def send(
self,
content: Optional[str] = None,
return_message: Literal[True] = True,
ephemeral: bool = False,
**kwargs: Any,
) -> Union[Message, WebhookMessage]:
...
async def send(
self, content: Optional[str] = None, return_message: bool = True, ephemeral: bool = False, **kwargs: Any
) -> Optional[Union[Message, WebhookMessage]]:
"""
|coro|
A shortcut method to :meth:`.abc.Messageable.send` with interaction helpers.
This function takes all the parameters of :meth:`.abc.Messageable.send` plus the following:
Parameters
------------
return_message: :class:`bool`
Ignored if not in a slash command context.
If this is set to False more native interaction methods will be used.
ephemeral: :class:`bool`
Ignored if not in a slash command context.
Indicates if the message should only be visible to the user who started the interaction.
If a view is sent with an ephemeral message and it has no timeout set then the timeout
is set to 15 minutes.
Returns
--------
Optional[Union[:class:`.Message`, :class:`.WebhookMessage`]]
In a slash command context, the message that was sent if return_message is True.
In a normal context, it always returns a :class:`.Message`
"""
if self._typing_task is not None:
self._typing_task.cancel()
self._typing_task = None
if self.interaction is None or (
self.interaction.response.responded_at is not None
and discord.utils.utcnow() - self.interaction.response.responded_at >= timedelta(minutes=15)
):
return await super().send(content, **kwargs)
# Remove unsupported arguments from kwargs
kwargs.pop("nonce", None)
kwargs.pop("stickers", None)
kwargs.pop("reference", None)
kwargs.pop("delete_after", None)
kwargs.pop("mention_author", None)
if not (
return_message
or self.interaction.response.is_done()
or any(arg in kwargs for arg in ("file", "files", "allowed_mentions"))
):
send = self.interaction.response.send_message
else:
# We have to defer in order to use the followup webhook
if not self.interaction.response.is_done():
await self.interaction.response.defer(ephemeral=ephemeral)
send = self.interaction.followup.send
return await send(content, ephemeral=ephemeral, **kwargs) # type: ignore
@overload
async def reply(
self, content: Optional[str] = None, return_message: Literal[False] = False, **kwargs: Any
) -> Optional[Union[Message, WebhookMessage]]:
...
@overload
async def reply(
self, content: Optional[str] = None, return_message: Literal[True] = True, **kwargs: Any
) -> Union[Message, WebhookMessage]:
...
@discord.utils.copy_doc(Message.reply)
async def reply(
self, content: Optional[str] = None, return_message: bool = True, **kwargs: Any
) -> Optional[Union[Message, WebhookMessage]]:
return await self.send(content, return_message=return_message, reference=self.message, **kwargs) # type: ignore
async def defer(self, *, ephemeral: bool = False, trigger_typing: bool = True) -> None:
"""|coro|
Defers the Slash Command interaction if ran in a slash command **or**
Loops triggering ``Bot is typing`` in the channel if run in a message command.
Parameters
------------
trigger_typing: :class:`bool`
Indicates whether to trigger typing in a message command.
ephemeral: :class:`bool`
Indicates whether the deferred message will eventually be ephemeral in a slash command.
"""
if self.interaction is None:
if self._typing_task is None and trigger_typing:
async def typing_task():
while True:
await self.trigger_typing()
await asyncio.sleep(10)
self._typing_task = self.bot.loop.create_task(typing_task())
else:
await self.interaction.response.defer(ephemeral=ephemeral)

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Any, Callable, Deque, Dict, Optional, Type, TypeVar, TYPE_CHECKING
from discord.enums import Enum
import time
import asyncio
@ -30,23 +34,31 @@ from collections import deque
from ...abc import PrivateChannel
from .errors import MaxConcurrencyReached
if TYPE_CHECKING:
from ...message import Message
__all__ = (
'BucketType',
'Cooldown',
'CooldownMapping',
'MaxConcurrency',
"BucketType",
"Cooldown",
"CooldownMapping",
"DynamicCooldownMapping",
"MaxConcurrency",
)
class BucketType(Enum):
default = 0
user = 1
guild = 2
channel = 3
member = 4
category = 5
role = 6
C = TypeVar("C", bound="CooldownMapping")
MC = TypeVar("MC", bound="MaxConcurrency")
def get_key(self, msg):
class BucketType(Enum):
default = 0
user = 1
guild = 2
channel = 3
member = 4
category = 5
role = 6
def get_key(self, msg: Message) -> Any:
if self is BucketType.user:
return msg.author.id
elif self is BucketType.guild:
@ -56,33 +68,52 @@ class BucketType(Enum):
elif self is BucketType.member:
return ((msg.guild and msg.guild.id), msg.author.id)
elif self is BucketType.category:
return (msg.channel.category or msg.channel).id
return (msg.channel.category or msg.channel).id # type: ignore
elif self is BucketType.role:
# we return the channel id of a private-channel as there are only roles in guilds
# and that yields the same result as for a guild with only the @everyone role
# NOTE: PrivateChannel doesn't actually have an id attribute but we assume we are
# recieving a DMChannel or GroupChannel which inherit from PrivateChannel and do
return (msg.channel if isinstance(msg.channel, PrivateChannel) else msg.author.top_role).id
return (msg.channel if isinstance(msg.channel, PrivateChannel) else msg.author.top_role).id # type: ignore
def __call__(self, msg):
def __call__(self, msg: Message) -> Any:
return self.get_key(msg)
class Cooldown:
__slots__ = ('rate', 'per', 'type', '_window', '_tokens', '_last')
"""Represents a cooldown for a command.
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
Attributes
-----------
rate: :class:`int`
The total number of tokens available per :attr:`per` seconds.
per: :class:`float`
The length of the cooldown period in seconds.
"""
if not callable(self.type):
raise TypeError('Cooldown type must be a BucketType or callable')
__slots__ = ("rate", "per", "_window", "_tokens", "_last")
def get_tokens(self, current=None):
def __init__(self, rate: float, per: float) -> None:
self.rate: int = int(rate)
self.per: float = float(per)
self._window: float = 0.0
self._tokens: int = self.rate
self._last: float = 0.0
def get_tokens(self, current: Optional[float] = None) -> int:
"""Returns the number of available tokens before rate limiting is applied.
Parameters
------------
current: Optional[:class:`float`]
The time in seconds since Unix epoch to calculate tokens at.
If not supplied then :func:`time.time()` is used.
Returns
--------
:class:`int`
The number of tokens available before the cooldown is to be applied.
"""
if not current:
current = time.time()
@ -92,7 +123,20 @@ class Cooldown:
tokens = self.rate
return tokens
def get_retry_after(self, current=None):
def get_retry_after(self, current: Optional[float] = None) -> float:
"""Returns the time in seconds until the cooldown will be reset.
Parameters
-------------
current: Optional[:class:`float`]
The current time in seconds since Unix epoch.
If not supplied, then :func:`time.time()` is used.
Returns
-------
:class:`float`
The number of seconds to wait before this cooldown will be reset.
"""
current = current or time.time()
tokens = self.get_tokens(current)
@ -101,7 +145,20 @@ class Cooldown:
return 0.0
def update_rate_limit(self, current=None):
def update_rate_limit(self, current: Optional[float] = None) -> Optional[float]:
"""Updates the cooldown rate limit.
Parameters
-------------
current: Optional[:class:`float`]
The time in seconds since Unix epoch to update the rate limit at.
If not supplied, then :func:`time.time()` is used.
Returns
-------
Optional[:class:`float`]
The retry-after time in seconds if rate limited.
"""
current = current or time.time()
self._last = current
@ -118,43 +175,59 @@ class Cooldown:
# 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):
def reset(self) -> None:
"""Reset the cooldown to its initial state."""
self._tokens = self.rate
self._last = 0.0
def copy(self):
return Cooldown(self.rate, self.per, self.type)
def copy(self) -> Cooldown:
"""Creates a copy of this cooldown.
Returns
--------
:class:`Cooldown`
A new instance of this cooldown.
"""
return Cooldown(self.rate, self.per)
def __repr__(self) -> str:
return f"<Cooldown rate: {self.rate} per: {self.per} window: {self._window} tokens: {self._tokens}>"
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
def __init__(
self,
original: Optional[Cooldown],
type: Callable[[Message], Any],
) -> None:
if not callable(type):
raise TypeError("Cooldown type must be a BucketType or callable")
def copy(self):
ret = CooldownMapping(self._cooldown)
self._cache: Dict[Any, Cooldown] = {}
self._cooldown: Optional[Cooldown] = original
self._type: Callable[[Message], Any] = type
def copy(self) -> CooldownMapping:
ret = CooldownMapping(self._cooldown, self._type)
ret._cache = self._cache.copy()
return ret
@property
def valid(self):
def valid(self) -> bool:
return self._cooldown is not None
@property
def type(self) -> Callable[[Message], Any]:
return self._type
@classmethod
def from_cooldown(cls, rate, per, type):
return cls(Cooldown(rate, per, type))
def from_cooldown(cls: Type[C], rate, per, type) -> C:
return cls(Cooldown(rate, per), type)
def _bucket_key(self, msg):
return self._cooldown.type(msg)
def _bucket_key(self, msg: Message) -> Any:
return self._type(msg)
def _verify_cache_integrity(self, current=None):
def _verify_cache_integrity(self, current: Optional[float] = None) -> None:
# 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
@ -163,24 +236,47 @@ class CooldownMapping:
for k in dead_keys:
del self._cache[k]
def get_bucket(self, message, current=None):
if self._cooldown.type is BucketType.default:
return self._cooldown
def create_bucket(self, message: Message) -> Cooldown:
return self._cooldown.copy() # type: ignore
def get_bucket(self, message: Message, current: Optional[float] = None) -> Cooldown:
if self._type is BucketType.default:
return self._cooldown # type: ignore
self._verify_cache_integrity(current)
key = self._bucket_key(message)
if key not in self._cache:
bucket = self._cooldown.copy()
self._cache[key] = bucket
bucket = self.create_bucket(message)
if bucket is not None:
self._cache[key] = bucket
else:
bucket = self._cache[key]
return bucket
def update_rate_limit(self, message, current=None):
def update_rate_limit(self, message: Message, current: Optional[float] = None) -> Optional[float]:
bucket = self.get_bucket(message, current)
return bucket.update_rate_limit(current)
class DynamicCooldownMapping(CooldownMapping):
def __init__(self, factory: Callable[[Message], Cooldown], type: Callable[[Message], Any]) -> None:
super().__init__(None, type)
self._factory: Callable[[Message], Cooldown] = factory
def copy(self) -> DynamicCooldownMapping:
ret = DynamicCooldownMapping(self._factory, self._type)
ret._cache = self._cache.copy()
return ret
@property
def valid(self) -> bool:
return True
def create_bucket(self, message: Message) -> Cooldown:
return self._factory(message)
class _Semaphore:
"""This class is a version of a semaphore.
@ -194,30 +290,30 @@ class _Semaphore:
overkill for what is basically a counter.
"""
__slots__ = ('value', 'loop', '_waiters')
__slots__ = ("value", "loop", "_waiters")
def __init__(self, number):
self.value = number
self.loop = asyncio.get_event_loop()
self._waiters = deque()
def __init__(self, number: int) -> None:
self.value: int = number
self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
self._waiters: Deque[asyncio.Future] = deque()
def __repr__(self):
return '<_Semaphore value={0.value} waiters={1}>'.format(self, len(self._waiters))
def __repr__(self) -> str:
return f"<_Semaphore value={self.value} waiters={len(self._waiters)}>"
def locked(self):
def locked(self) -> bool:
return self.value == 0
def is_active(self):
def is_active(self) -> bool:
return len(self._waiters) > 0
def wake_up(self):
def wake_up(self) -> None:
while self._waiters:
future = self._waiters.popleft()
if not future.done():
future.set_result(None)
return
async def acquire(self, *, wait=False):
async def acquire(self, *, wait: bool = False) -> bool:
if not wait and self.value <= 0:
# signal that we're not acquiring
return False
@ -236,35 +332,36 @@ class _Semaphore:
self.value -= 1
return True
def release(self):
def release(self) -> None:
self.value += 1
self.wake_up()
class MaxConcurrency:
__slots__ = ('number', 'per', 'wait', '_mapping')
def __init__(self, number, *, per, wait):
self._mapping = {}
self.per = per
self.number = number
self.wait = wait
class MaxConcurrency:
__slots__ = ("number", "per", "wait", "_mapping")
def __init__(self, number: int, *, per: BucketType, wait: bool) -> None:
self._mapping: Dict[Any, _Semaphore] = {}
self.per: BucketType = per
self.number: int = number
self.wait: bool = wait
if number <= 0:
raise ValueError('max_concurrency \'number\' cannot be less than 1')
raise ValueError("max_concurrency 'number' cannot be less than 1")
if not isinstance(per, BucketType):
raise TypeError(f'max_concurrency \'per\' must be of type BucketType not {type(per)!r}')
raise TypeError(f"max_concurrency 'per' must be of type BucketType not {type(per)!r}")
def copy(self):
def copy(self: MC) -> MC:
return self.__class__(self.number, per=self.per, wait=self.wait)
def __repr__(self):
return '<MaxConcurrency per={0.per!r} number={0.number} wait={0.wait}>'.format(self)
def __repr__(self) -> str:
return f"<MaxConcurrency per={self.per!r} number={self.number} wait={self.wait}>"
def get_key(self, message):
def get_key(self, message: Message) -> Any:
return self.per.get_key(message)
async def acquire(self, message):
async def acquire(self, message: Message) -> None:
key = self.get_key(message)
try:
@ -276,7 +373,7 @@ class MaxConcurrency:
if not acquired:
raise MaxConcurrencyReached(self.number, self.per)
async def release(self, message):
async def release(self, message: Message) -> None:
# Technically there's no reason for this function to be async
# But it might be more useful in the future
key = self.get_key(message)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,630 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from .errors import (
BadFlagArgument,
CommandError,
MissingFlagArgument,
TooManyFlags,
MissingRequiredFlag,
)
from discord.utils import resolve_annotation
from .view import StringView
from .converter import run_converters
from discord.utils import maybe_coroutine, MISSING
from dataclasses import dataclass, field
from typing import (
Dict,
Iterator,
Literal,
Optional,
Pattern,
Set,
TYPE_CHECKING,
Tuple,
List,
Any,
Type,
TypeVar,
Union,
)
import inspect
import sys
import re
__all__ = (
"Flag",
"flag",
"FlagConverter",
)
if TYPE_CHECKING:
from .context import Context
@dataclass
class Flag:
"""Represents a flag parameter for :class:`FlagConverter`.
The :func:`~discord.ext.commands.flag` function helps
create these flag objects, but it is not necessary to
do so. These cannot be constructed manually.
Attributes
------------
name: :class:`str`
The name of the flag.
description: :class:`str`
The description of the flag.
aliases: List[:class:`str`]
The aliases of the flag name.
attribute: :class:`str`
The attribute in the class that corresponds to this flag.
default: Any
The default value of the flag, if available.
annotation: Any
The underlying evaluated annotation of the flag.
max_args: :class:`int`
The maximum number of arguments the flag can accept.
A negative value indicates an unlimited amount of arguments.
override: :class:`bool`
Whether multiple given values overrides the previous value.
"""
name: str = MISSING
description: str = MISSING
aliases: List[str] = field(default_factory=list)
attribute: str = MISSING
annotation: Any = MISSING
default: Any = MISSING
max_args: int = MISSING
override: bool = MISSING
cast_to_dict: bool = False
@property
def required(self) -> bool:
""":class:`bool`: Whether the flag is required.
A required flag has no default value.
"""
return self.default is MISSING
def flag(
*,
name: str = MISSING,
description: str = MISSING,
aliases: List[str] = MISSING,
default: Any = MISSING,
max_args: int = MISSING,
override: bool = MISSING,
) -> Any:
"""Override default functionality and parameters of the underlying :class:`FlagConverter`
class attributes.
Parameters
------------
name: :class:`str`
The flag name. If not given, defaults to the attribute name.
description: :class:`str`
Description of the flag for the slash commands options. The default value is `'no description'`.
aliases: List[:class:`str`]
Aliases to the flag name. If not given no aliases are set.
default: Any
The default parameter. This could be either a value or a callable that takes
:class:`Context` as its sole parameter. If not given then it defaults to
the default value given to the attribute.
max_args: :class:`int`
The maximum number of arguments the flag can accept.
A negative value indicates an unlimited amount of arguments.
The default value depends on the annotation given.
override: :class:`bool`
Whether multiple given values overrides the previous value. The default
value depends on the annotation given.
"""
return Flag(
name=name, description=description, aliases=aliases, default=default, max_args=max_args, override=override
)
def validate_flag_name(name: str, forbidden: Set[str]):
if not name:
raise ValueError("flag names should not be empty")
for ch in name:
if ch.isspace():
raise ValueError(f"flag name {name!r} cannot have spaces")
if ch == "\\":
raise ValueError(f"flag name {name!r} cannot have backslashes")
if ch in forbidden:
raise ValueError(f"flag name {name!r} cannot have any of {forbidden!r} within them")
def get_flags(namespace: Dict[str, Any], globals: Dict[str, Any], locals: Dict[str, Any]) -> Dict[str, Flag]:
annotations = namespace.get("__annotations__", {})
case_insensitive = namespace["__commands_flag_case_insensitive__"]
flags: Dict[str, Flag] = {}
cache: Dict[str, Any] = {}
names: Set[str] = set()
for name, annotation in annotations.items():
flag = namespace.pop(name, MISSING)
if isinstance(flag, Flag):
flag.annotation = annotation
else:
flag = Flag(name=name, annotation=annotation, default=flag)
flag.attribute = name
if flag.name is MISSING:
flag.name = name
annotation = flag.annotation = resolve_annotation(flag.annotation, globals, locals, cache)
if (
flag.default is MISSING
and hasattr(annotation, "__commands_is_flag__")
and annotation._can_be_constructible()
):
flag.default = annotation._construct_default
if flag.aliases is MISSING:
flag.aliases = []
# Add sensible defaults based off of the type annotation
# <type> -> (max_args=1)
# List[str] -> (max_args=-1)
# Tuple[int, ...] -> (max_args=1)
# Dict[K, V] -> (max_args=-1, override=True)
# Union[str, int] -> (max_args=1)
# Optional[str] -> (default=None, max_args=1)
try:
origin = annotation.__origin__
except AttributeError:
# A regular type hint
if flag.max_args is MISSING:
flag.max_args = 1
else:
if origin is Union:
# typing.Union
if flag.max_args is MISSING:
flag.max_args = 1
if annotation.__args__[-1] is type(None) and flag.default is MISSING:
# typing.Optional
flag.default = None
elif origin is tuple:
# typing.Tuple
# tuple parsing is e.g. `flag: peter 20`
# for Tuple[str, int] would give you flag: ('peter', 20)
if flag.max_args is MISSING:
flag.max_args = 1
elif origin is list:
# typing.List
if flag.max_args is MISSING:
flag.max_args = -1
elif origin is dict:
# typing.Dict[K, V]
# Equivalent to:
# typing.List[typing.Tuple[K, V]]
flag.cast_to_dict = True
if flag.max_args is MISSING:
flag.max_args = -1
if flag.override is MISSING:
flag.override = True
elif origin is Literal:
if flag.max_args is MISSING:
flag.max_args = 1
else:
raise TypeError(f"Unsupported typing annotation {annotation!r} for {flag.name!r} flag")
if flag.override is MISSING:
flag.override = False
# Validate flag names are unique
name = flag.name.casefold() if case_insensitive else flag.name
if name in names:
raise TypeError(f"{flag.name!r} flag conflicts with previous flag or alias.")
else:
names.add(name)
for alias in flag.aliases:
# Validate alias is unique
alias = alias.casefold() if case_insensitive else alias
if alias in names:
raise TypeError(f"{flag.name!r} flag alias {alias!r} conflicts with previous flag or alias.")
else:
names.add(alias)
flags[flag.name] = flag
return flags
class FlagsMeta(type):
if TYPE_CHECKING:
__commands_is_flag__: bool
__commands_flags__: Dict[str, Flag]
__commands_flag_aliases__: Dict[str, str]
__commands_flag_regex__: Pattern[str]
__commands_flag_case_insensitive__: bool
__commands_flag_delimiter__: str
__commands_flag_prefix__: str
def __new__(
cls: Type[type],
name: str,
bases: Tuple[type, ...],
attrs: Dict[str, Any],
*,
case_insensitive: bool = MISSING,
delimiter: str = MISSING,
prefix: str = MISSING,
):
attrs["__commands_is_flag__"] = True
try:
global_ns = sys.modules[attrs["__module__"]].__dict__
except KeyError:
global_ns = {}
frame = inspect.currentframe()
try:
if frame is None:
local_ns = {}
else:
if frame.f_back is None:
local_ns = frame.f_locals
else:
local_ns = frame.f_back.f_locals
finally:
del frame
flags: Dict[str, Flag] = {}
aliases: Dict[str, str] = {}
for base in reversed(bases):
if base.__dict__.get("__commands_is_flag__", False):
flags.update(base.__dict__["__commands_flags__"])
aliases.update(base.__dict__["__commands_flag_aliases__"])
if case_insensitive is MISSING:
attrs["__commands_flag_case_insensitive__"] = base.__dict__["__commands_flag_case_insensitive__"]
if delimiter is MISSING:
attrs["__commands_flag_delimiter__"] = base.__dict__["__commands_flag_delimiter__"]
if prefix is MISSING:
attrs["__commands_flag_prefix__"] = base.__dict__["__commands_flag_prefix__"]
if case_insensitive is not MISSING:
attrs["__commands_flag_case_insensitive__"] = case_insensitive
if delimiter is not MISSING:
attrs["__commands_flag_delimiter__"] = delimiter
if prefix is not MISSING:
attrs["__commands_flag_prefix__"] = prefix
case_insensitive = attrs.setdefault("__commands_flag_case_insensitive__", False)
delimiter = attrs.setdefault("__commands_flag_delimiter__", ":")
prefix = attrs.setdefault("__commands_flag_prefix__", "")
for flag_name, flag in get_flags(attrs, global_ns, local_ns).items():
flags[flag_name] = flag
aliases.update({alias_name: flag_name for alias_name in flag.aliases})
forbidden = set(delimiter).union(prefix)
for flag_name in flags:
validate_flag_name(flag_name, forbidden)
for alias_name in aliases:
validate_flag_name(alias_name, forbidden)
regex_flags = 0
if case_insensitive:
flags = {key.casefold(): value for key, value in flags.items()}
aliases = {key.casefold(): value.casefold() for key, value in aliases.items()}
regex_flags = re.IGNORECASE
keys = list(re.escape(k) for k in flags)
keys.extend(re.escape(a) for a in aliases)
keys = sorted(keys, key=lambda t: len(t), reverse=True)
joined = "|".join(keys)
pattern = re.compile(f"(({re.escape(prefix)})(?P<flag>{joined}){re.escape(delimiter)})", regex_flags)
attrs["__commands_flag_regex__"] = pattern
attrs["__commands_flags__"] = flags
attrs["__commands_flag_aliases__"] = aliases
return type.__new__(cls, name, bases, attrs)
async def tuple_convert_all(ctx: Context, argument: str, flag: Flag, converter: Any) -> Tuple[Any, ...]:
view = StringView(argument)
results = []
param: inspect.Parameter = ctx.current_parameter # type: ignore
while not view.eof:
view.skip_ws()
if view.eof:
break
word = view.get_quoted_word()
if word is None:
break
try:
converted = await run_converters(ctx, converter, word, param)
except CommandError:
raise
except Exception as e:
raise BadFlagArgument(flag) from e
else:
results.append(converted)
return tuple(results)
async def tuple_convert_flag(ctx: Context, argument: str, flag: Flag, converters: Any) -> Tuple[Any, ...]:
view = StringView(argument)
results = []
param: inspect.Parameter = ctx.current_parameter # type: ignore
for converter in converters:
view.skip_ws()
if view.eof:
break
word = view.get_quoted_word()
if word is None:
break
try:
converted = await run_converters(ctx, converter, word, param)
except CommandError:
raise
except Exception as e:
raise BadFlagArgument(flag) from e
else:
results.append(converted)
if len(results) != len(converters):
raise BadFlagArgument(flag)
return tuple(results)
async def convert_flag(ctx, argument: str, flag: Flag, annotation: Any = None) -> Any:
param: inspect.Parameter = ctx.current_parameter # type: ignore
annotation = annotation or flag.annotation
try:
origin = annotation.__origin__
except AttributeError:
pass
else:
if origin is tuple:
if annotation.__args__[-1] is Ellipsis:
return await tuple_convert_all(ctx, argument, flag, annotation.__args__[0])
else:
return await tuple_convert_flag(ctx, argument, flag, annotation.__args__)
elif origin is list:
# typing.List[x]
annotation = annotation.__args__[0]
return await convert_flag(ctx, argument, flag, annotation)
elif origin is Union and annotation.__args__[-1] is type(None):
# typing.Optional[x]
annotation = Union[annotation.__args__[:-1]]
return await run_converters(ctx, annotation, argument, param)
elif origin is dict:
# typing.Dict[K, V] -> typing.Tuple[K, V]
return await tuple_convert_flag(ctx, argument, flag, annotation.__args__)
try:
return await run_converters(ctx, annotation, argument, param)
except CommandError:
raise
except Exception as e:
raise BadFlagArgument(flag) from e
F = TypeVar("F", bound="FlagConverter")
class FlagConverter(metaclass=FlagsMeta):
"""A converter that allows for a user-friendly flag syntax.
The flags are defined using :pep:`526` type annotations similar
to the :mod:`dataclasses` Python module. For more information on
how this converter works, check the appropriate
:ref:`documentation <ext_commands_flag_converter>`.
.. container:: operations
.. describe:: iter(x)
Returns an iterator of ``(flag_name, flag_value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. versionadded:: 2.0
Parameters
-----------
case_insensitive: :class:`bool`
A class parameter to toggle case insensitivity of the flag parsing.
If ``True`` then flags are parsed in a case insensitive manner.
Defaults to ``False``.
prefix: :class:`str`
The prefix that all flags must be prefixed with. By default
there is no prefix.
delimiter: :class:`str`
The delimiter that separates a flag's argument from the flag's name.
By default this is ``:``.
"""
@classmethod
def get_flags(cls) -> Dict[str, Flag]:
"""Dict[:class:`str`, :class:`Flag`]: A mapping of flag name to flag object this converter has."""
return cls.__commands_flags__.copy()
@classmethod
def _can_be_constructible(cls) -> bool:
return all(not flag.required for flag in cls.__commands_flags__.values())
def __iter__(self) -> Iterator[Tuple[str, Any]]:
for flag in self.__class__.__commands_flags__.values():
yield (flag.name, getattr(self, flag.attribute))
@classmethod
async def _construct_default(cls: Type[F], ctx: Context) -> F:
self: F = cls.__new__(cls)
flags = cls.__commands_flags__
for flag in flags.values():
if callable(flag.default):
default = await maybe_coroutine(flag.default, ctx)
setattr(self, flag.attribute, default)
else:
setattr(self, flag.attribute, flag.default)
return self
def __repr__(self) -> str:
pairs = " ".join([f"{flag.attribute}={getattr(self, flag.attribute)!r}" for flag in self.get_flags().values()])
return f"<{self.__class__.__name__} {pairs}>"
@classmethod
def parse_flags(cls, argument: str) -> Dict[str, List[str]]:
result: Dict[str, List[str]] = {}
flags = cls.__commands_flags__
aliases = cls.__commands_flag_aliases__
last_position = 0
last_flag: Optional[Flag] = None
case_insensitive = cls.__commands_flag_case_insensitive__
for match in cls.__commands_flag_regex__.finditer(argument):
begin, end = match.span(0)
key = match.group("flag")
if case_insensitive:
key = key.casefold()
if key in aliases:
key = aliases[key]
flag = flags.get(key)
if last_position and last_flag is not None:
value = argument[last_position : begin - 1].lstrip()
if not value:
raise MissingFlagArgument(last_flag)
try:
values = result[last_flag.name]
except KeyError:
result[last_flag.name] = [value]
else:
values.append(value)
last_position = end
last_flag = flag
# Add the remaining string to the last available flag
if last_position and last_flag is not None:
value = argument[last_position:].strip()
if not value:
raise MissingFlagArgument(last_flag)
try:
values = result[last_flag.name]
except KeyError:
result[last_flag.name] = [value]
else:
values.append(value)
# Verification of values will come at a later stage
return result
@classmethod
async def convert(cls: Type[F], ctx: Context, argument: str) -> F:
"""|coro|
The method that actually converters an argument to the flag mapping.
Parameters
----------
cls: Type[:class:`FlagConverter`]
The flag converter class.
ctx: :class:`Context`
The invocation context.
argument: :class:`str`
The argument to convert from.
Raises
--------
FlagError
A flag related parsing error.
CommandError
A command related error.
Returns
--------
:class:`FlagConverter`
The flag converter instance with all flags parsed.
"""
arguments = cls.parse_flags(argument)
flags = cls.__commands_flags__
self: F = cls.__new__(cls)
for name, flag in flags.items():
try:
values = arguments[name]
except KeyError:
if flag.required:
raise MissingRequiredFlag(flag)
else:
if callable(flag.default):
default = await maybe_coroutine(flag.default, ctx)
setattr(self, flag.attribute, default)
else:
setattr(self, flag.attribute, flag.default)
continue
if flag.max_args > 0 and len(values) > flag.max_args:
if flag.override:
values = values[-flag.max_args :]
else:
raise TooManyFlags(flag, values)
# Special case:
if flag.max_args == 1:
value = await convert_flag(ctx, values[0], flag)
setattr(self, flag.attribute, value)
continue
# Another special case, tuple parsing.
# Tuple parsing is basically converting arguments within the flag
# So, given flag: hello 20 as the input and Tuple[str, int] as the type hint
# We would receive ('hello', 20) as the resulting value
# This uses the same whitespace and quoting rules as regular parameters.
values = [await convert_flag(ctx, value, flag) for value in values]
if flag.cast_to_dict:
values = dict(values) # type: ignore
setattr(self, flag.attribute, values)
return self

View File

@ -27,16 +27,22 @@ import copy
import functools
import inspect
import re
from typing import Optional, TYPE_CHECKING
import discord.utils
from .core import Group, Command
from .errors import CommandError
if TYPE_CHECKING:
from .context import Context
__all__ = (
'Paginator',
'HelpCommand',
'DefaultHelpCommand',
'MinimalHelpCommand',
"Paginator",
"HelpCommand",
"DefaultHelpCommand",
"MinimalHelpCommand",
)
# help -> shows info of bot on top/bottom and lists subcommands
@ -60,6 +66,7 @@ __all__ = (
# 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.
@ -81,7 +88,8 @@ class Paginator:
The character string inserted between lines. e.g. a newline character.
.. versionadded:: 1.7
"""
def __init__(self, prefix='```', suffix='```', max_size=2000, linesep='\n'):
def __init__(self, prefix="```", suffix="```", max_size=2000, linesep="\n"):
self.prefix = prefix
self.suffix = suffix
self.max_size = max_size
@ -92,7 +100,7 @@ class Paginator:
"""Clears the paginator to have no pages."""
if self.prefix is not None:
self._current_page = [self.prefix]
self._count = len(self.prefix) + self._linesep_len # prefix + newline
self._count = len(self.prefix) + self._linesep_len # prefix + newline
else:
self._current_page = []
self._count = 0
@ -110,7 +118,7 @@ class Paginator:
def _linesep_len(self):
return len(self.linesep)
def add_line(self, line='', *, empty=False):
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
@ -130,7 +138,7 @@ class Paginator:
"""
max_page_size = self.max_size - self._prefix_len - self._suffix_len - 2 * self._linesep_len
if len(line) > max_page_size:
raise RuntimeError(f'Line exceeds maximum page size {max_page_size}')
raise RuntimeError(f"Line exceeds maximum page size {max_page_size}")
if self._count + len(line) + self._linesep_len > self.max_size - self._suffix_len:
self.close_page()
@ -139,7 +147,7 @@ class Paginator:
self._current_page.append(line)
if empty:
self._current_page.append('')
self._current_page.append("")
self._count += self._linesep_len
def close_page(self):
@ -150,7 +158,7 @@ class Paginator:
if self.prefix is not None:
self._current_page = [self.prefix]
self._count = len(self.prefix) + self._linesep_len # prefix + linesep
self._count = len(self.prefix) + self._linesep_len # prefix + linesep
else:
self._current_page = []
self._count = 0
@ -168,13 +176,15 @@ class Paginator:
return self._pages
def __repr__(self):
fmt = '<Paginator prefix: {0.prefix!r} suffix: {0.suffix!r} linesep: {0.linesep!r} max_size: {0.max_size} count: {0._count}>'
fmt = "<Paginator prefix: {0.prefix!r} suffix: {0.suffix!r} linesep: {0.linesep!r} max_size: {0.max_size} count: {0._count}>"
return fmt.format(self)
def _not_overriden(f):
f.__help_command_not_overriden__ = True
return f
class _HelpCommandImpl(Command):
def __init__(self, inject, *args, **kwargs):
super().__init__(inject.command_callback, *args, **kwargs)
@ -187,7 +197,7 @@ class _HelpCommandImpl(Command):
self.callback = injected.command_callback
on_error = injected.on_help_command_error
if not hasattr(on_error, '__help_command_not_overriden__'):
if not hasattr(on_error, "__help_command_not_overriden__"):
if self.cog is not None:
self.on_error = self._on_error_cog_implementation
else:
@ -212,9 +222,9 @@ class _HelpCommandImpl(Command):
def clean_params(self):
result = self.params.copy()
try:
result.popitem(last=False)
except Exception:
raise ValueError('Missing context parameter') from None
del result[next(iter(result))]
except StopIteration:
raise ValueError("Missing context parameter") from None
else:
return result
@ -250,6 +260,7 @@ class _HelpCommandImpl(Command):
cog.walk_commands = cog.walk_commands.__wrapped__
self.cog = None
class HelpCommand:
r"""The base implementation for help command formatting.
@ -272,11 +283,11 @@ class HelpCommand:
Defaults to ``False``.
verify_checks: Optional[:class:`bool`]
Specifies if commands should have their :attr:`.Command.checks` called
and verified. If ``True``, always calls :attr:`.Commands.checks`.
If ``None``, only calls :attr:`.Commands.checks` in a guild setting.
If ``False``, never calls :attr:`.Commands.checks`. Defaults to ``True``.
and verified. If ``True``, always calls :attr:`.Command.checks`.
If ``None``, only calls :attr:`.Command.checks` in a guild setting.
If ``False``, never calls :attr:`.Command.checks`. Defaults to ``True``.
..versionchanged:: 1.7
.. versionchanged:: 1.7
command_attrs: :class:`dict`
A dictionary of options to pass in for the construction of the help command.
This allows you to change the command behaviour without actually changing
@ -285,13 +296,13 @@ class HelpCommand:
"""
MENTION_TRANSFORMS = {
'@everyone': '@\u200beveryone',
'@here': '@\u200bhere',
r'<@!?[0-9]{17,22}>': '@deleted-user',
r'<@&[0-9]{17,22}>': '@deleted-role'
"@everyone": "@\u200beveryone",
"@here": "@\u200bhere",
r"<@!?[0-9]{17,22}>": "@deleted-user",
r"<@&[0-9]{17,22}>": "@deleted-role",
}
MENTION_PATTERN = re.compile('|'.join(MENTION_TRANSFORMS.keys()))
MENTION_PATTERN = re.compile("|".join(MENTION_TRANSFORMS.keys()))
def __new__(cls, *args, **kwargs):
# To prevent race conditions of a single instance while also allowing
@ -305,20 +316,17 @@ class HelpCommand:
# The keys can be safely copied as-is since they're 99.99% certain of being
# string keys
deepcopy = copy.deepcopy
self.__original_kwargs__ = {
k: deepcopy(v)
for k, v in kwargs.items()
}
self.__original_kwargs__ = {k: deepcopy(v) for k, v in kwargs.items()}
self.__original_args__ = deepcopy(args)
return self
def __init__(self, **options):
self.show_hidden = options.pop('show_hidden', False)
self.verify_checks = options.pop('verify_checks', True)
self.command_attrs = attrs = options.pop('command_attrs', {})
attrs.setdefault('name', 'help')
attrs.setdefault('help', 'Shows this message')
self.context = None
self.show_hidden = options.pop("show_hidden", False)
self.verify_checks = options.pop("verify_checks", True)
self.command_attrs = attrs = options.pop("command_attrs", {})
attrs.setdefault("name", "help")
attrs.setdefault("help", "Shows this message")
self.context: Context = discord.utils.MISSING
self._command_impl = _HelpCommandImpl(self, **self.command_attrs)
def copy(self):
@ -369,25 +377,10 @@ class HelpCommand:
def get_bot_mapping(self):
"""Retrieves the bot mapping passed to :meth:`send_bot_help`."""
bot = self.context.bot
mapping = {
cog: cog.get_commands()
for cog in bot.cogs.values()
}
mapping = {cog: cog.get_commands() for cog in bot.cogs.values()}
mapping[None] = [c for c in bot.commands if c.cog is None]
return mapping
@property
def clean_prefix(self):
""":class:`str`: 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.
pattern = re.compile(fr"<@!?{user.id}>")
display_name = user.display_name.replace('\\', r'\\')
return pattern.sub('@' + display_name, self.context.prefix)
@property
def invoked_with(self):
"""Similar to :attr:`Context.invoked_with` except properly handles
@ -429,20 +422,20 @@ class HelpCommand:
if not parent.signature or parent.invoke_without_command:
entries.append(parent.name)
else:
entries.append(parent.name + ' ' + parent.signature)
entries.append(parent.name + " " + parent.signature)
parent = parent.parent
parent_sig = ' '.join(reversed(entries))
parent_sig = " ".join(reversed(entries))
if len(command.aliases) > 0:
aliases = '|'.join(command.aliases)
fmt = f'[{command.name}|{aliases}]'
aliases = "|".join(command.aliases)
fmt = f"[{command.name}|{aliases}]"
if parent_sig:
fmt = parent_sig + ' ' + fmt
fmt = parent_sig + " " + fmt
alias = fmt
else:
alias = command.name if not parent_sig else parent_sig + ' ' + command.name
alias = command.name if not parent_sig else parent_sig + " " + command.name
return f'{self.clean_prefix}{alias} {command.signature}'
return f"{self.context.clean_prefix}{alias} {command.signature}"
def remove_mentions(self, string):
"""Removes mentions from the string to prevent abuse.
@ -456,7 +449,7 @@ class HelpCommand:
"""
def replace(obj, *, transforms=self.MENTION_TRANSFORMS):
return transforms.get(obj.group(0), '@invalid')
return transforms.get(obj.group(0), "@invalid")
return self.MENTION_PATTERN.sub(replace, string)
@ -607,10 +600,7 @@ class HelpCommand:
The maximum width of the commands.
"""
as_lengths = (
discord.utils._string_width(c.name)
for c in commands
)
as_lengths = (discord.utils._string_width(c.name) for c in commands)
return max(as_lengths, default=0)
def get_destination(self):
@ -625,14 +615,13 @@ class HelpCommand:
:class:`.abc.Messageable`
The destination where the help command will be output.
"""
return self.context.channel
return self.context
async def send_error_message(self, error):
"""|coro|
Handles the implementation when an error happens in the help command.
For example, the result of :meth:`command_not_found` or
:meth:`command_has_no_subcommand_found` will be passed here.
For example, the result of :meth:`command_not_found` will be passed here.
You can override this method to customise the behaviour.
@ -857,7 +846,7 @@ class HelpCommand:
# Since we want to have detailed errors when someone
# passes an invalid subcommand, we need to walk through
# the command group chain ourselves.
keys = command.split(' ')
keys = command.split(" ")
cmd = bot.all_commands.get(keys[0])
if cmd is None:
string = await maybe_coro(self.command_not_found, self.remove_mentions(keys[0]))
@ -880,6 +869,7 @@ class HelpCommand:
else:
return await self.send_command_help(cmd)
class DefaultHelpCommand(HelpCommand):
"""The implementation of the default help command.
@ -917,14 +907,14 @@ class DefaultHelpCommand(HelpCommand):
"""
def __init__(self, **options):
self.width = options.pop('width', 80)
self.indent = options.pop('indent', 2)
self.sort_commands = options.pop('sort_commands', True)
self.dm_help = options.pop('dm_help', False)
self.dm_help_threshold = options.pop('dm_help_threshold', 1000)
self.commands_heading = options.pop('commands_heading', "Commands:")
self.no_category = options.pop('no_category', 'No Category')
self.paginator = options.pop('paginator', None)
self.width = options.pop("width", 80)
self.indent = options.pop("indent", 2)
self.sort_commands = options.pop("sort_commands", True)
self.dm_help = options.pop("dm_help", False)
self.dm_help_threshold = options.pop("dm_help_threshold", 1000)
self.commands_heading = options.pop("commands_heading", "Commands:")
self.no_category = options.pop("no_category", "No Category")
self.paginator = options.pop("paginator", None)
if self.paginator is None:
self.paginator = Paginator()
@ -934,14 +924,16 @@ class DefaultHelpCommand(HelpCommand):
def shorten_text(self, text):
""":class:`str`: Shortens text to fit into the :attr:`width`."""
if len(text) > self.width:
return text[:self.width - 3] + '...'
return text[: self.width - 3].rstrip() + "..."
return text
def get_ending_note(self):
""":class:`str`: Returns help command's ending note. This is mainly useful to override for i18n purposes."""
command_name = self.invoked_with
return f"Type {self.clean_prefix}{command_name} command for more info on a command.\n" \
f"You can also type {self.clean_prefix}{command_name} category for more info on a category."
return (
f"Type {self.context.clean_prefix}{command_name} command for more info on a command.\n"
f"You can also type {self.context.clean_prefix}{command_name} category for more info on a category."
)
def add_indented_commands(self, commands, *, heading, max_size=None):
"""Indents a list of commands after the specified heading.
@ -962,7 +954,7 @@ class DefaultHelpCommand(HelpCommand):
if the list of commands is greater than 0.
max_size: Optional[:class:`int`]
The max size to use for the gap between indents.
If unspecified, calls :meth:`get_max_size` on the
If unspecified, calls :meth:`~HelpCommand.get_max_size` on the
commands parameter.
"""
@ -985,6 +977,10 @@ class DefaultHelpCommand(HelpCommand):
for page in self.paginator.pages:
await destination.send(page)
interaction = self.context.interaction
if interaction is not None and destination == self.context.author and not interaction.response.is_done():
await interaction.response.send_message("Sent help to your DMs!", ephemeral=True)
def add_command_formatting(self, command):
"""A utility function to format the non-indented block of commands and groups.
@ -1015,7 +1011,7 @@ class DefaultHelpCommand(HelpCommand):
elif self.dm_help is None and len(self.paginator) > self.dm_help_threshold:
return ctx.author
else:
return ctx.channel
return ctx
async def prepare_help_command(self, ctx, command):
self.paginator.clear()
@ -1029,10 +1025,11 @@ class DefaultHelpCommand(HelpCommand):
# <description> portion
self.paginator.add_line(bot.description, empty=True)
no_category = f'\u200b{self.no_category}:'
no_category = f"\u200b{self.no_category}:"
def get_category(command, *, no_category=no_category):
cog = command.cog
return cog.qualified_name + ':' if cog is not None else no_category
return cog.qualified_name + ":" if cog is not None else no_category
filtered = await self.filter_commands(bot.commands, sort=True, key=get_category)
max_size = self.get_max_size(filtered)
@ -1083,6 +1080,7 @@ class DefaultHelpCommand(HelpCommand):
await self.send_pages()
class MinimalHelpCommand(HelpCommand):
"""An implementation of a help command with minimal output.
@ -1116,13 +1114,13 @@ class MinimalHelpCommand(HelpCommand):
"""
def __init__(self, **options):
self.sort_commands = options.pop('sort_commands', True)
self.commands_heading = options.pop('commands_heading', "Commands")
self.dm_help = options.pop('dm_help', False)
self.dm_help_threshold = options.pop('dm_help_threshold', 1000)
self.aliases_heading = options.pop('aliases_heading', "Aliases:")
self.no_category = options.pop('no_category', 'No Category')
self.paginator = options.pop('paginator', None)
self.sort_commands = options.pop("sort_commands", True)
self.commands_heading = options.pop("commands_heading", "Commands")
self.dm_help = options.pop("dm_help", False)
self.dm_help_threshold = options.pop("dm_help_threshold", 1000)
self.aliases_heading = options.pop("aliases_heading", "Aliases:")
self.no_category = options.pop("no_category", "No Category")
self.paginator = options.pop("paginator", None)
if self.paginator is None:
self.paginator = Paginator(suffix=None, prefix=None)
@ -1149,11 +1147,13 @@ class MinimalHelpCommand(HelpCommand):
The help command opening note.
"""
command_name = self.invoked_with
return "Use `{0}{1} [command]` for more info on a command.\n" \
"You can also use `{0}{1} [category]` for more info on a category.".format(self.clean_prefix, command_name)
return (
f"Use `{self.context.clean_prefix}{command_name} [command]` for more info on a command.\n"
f"You can also use `{self.context.clean_prefix}{command_name} [category]` for more info on a category."
)
def get_command_signature(self, command):
return f'{self.clean_prefix}{command.qualified_name} {command.signature}'
return f"{self.context.clean_prefix}{command.qualified_name} {command.signature}"
def get_ending_note(self):
"""Return the help command's ending note. This is mainly useful to override for i18n purposes.
@ -1184,8 +1184,8 @@ class MinimalHelpCommand(HelpCommand):
"""
if commands:
# U+2002 Middle Dot
joined = '\u2002'.join(c.name for c in commands)
self.paginator.add_line(f'__**{heading}**__')
joined = "\u2002".join(c.name for c in commands)
self.paginator.add_line(f"__**{heading}**__")
self.paginator.add_line(joined)
def add_subcommand_formatting(self, command):
@ -1201,8 +1201,8 @@ class MinimalHelpCommand(HelpCommand):
command: :class:`Command`
The command to show information of.
"""
fmt = '{0}{1} \N{EN DASH} {2}' if command.short_doc else '{0}{1}'
self.paginator.add_line(fmt.format(self.clean_prefix, command.qualified_name, command.short_doc))
fmt = "{0}{1} \N{EN DASH} {2}" if command.short_doc else "{0}{1}"
self.paginator.add_line(fmt.format(self.context.clean_prefix, command.qualified_name, command.short_doc))
def add_aliases_formatting(self, aliases):
"""Adds the formatting information on a command's aliases.
@ -1272,7 +1272,8 @@ class MinimalHelpCommand(HelpCommand):
if note:
self.paginator.add_line(note, empty=True)
no_category = f'\u200b{self.no_category}'
no_category = f"\u200b{self.no_category}"
def get_category(command, *, no_category=no_category):
cog = command.cog
return cog.qualified_name if cog is not None else no_category
@ -1305,7 +1306,7 @@ class MinimalHelpCommand(HelpCommand):
filtered = await self.filter_commands(cog.get_commands(), sort=self.sort_commands)
if filtered:
self.paginator.add_line(f'**{cog.qualified_name} {self.commands_heading}**')
self.paginator.add_line(f"**{cog.qualified_name} {self.commands_heading}**")
for command in filtered:
self.add_subcommand_formatting(command)
@ -1325,7 +1326,7 @@ class MinimalHelpCommand(HelpCommand):
if note:
self.paginator.add_line(note, empty=True)
self.paginator.add_line(f'**{self.commands_heading}**')
self.paginator.add_line(f"**{self.commands_heading}**")
for command in filtered:
self.add_subcommand_formatting(command)

View File

@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE.
from .errors import UnexpectedQuoteError, InvalidEndOfQuotedStringError, ExpectedClosingQuoteError
# map from opening quotes to closing quotes
_quotes = {
supported_quotes = {
'"': '"',
"": "",
"": "",
@ -44,7 +44,8 @@ _quotes = {
"": "",
"": "",
}
_all_quotes = set(_quotes.keys()) | set(_quotes.values())
_all_quotes = set(supported_quotes.keys()) | set(supported_quotes.values())
class StringView:
def __init__(self, buffer):
@ -81,20 +82,20 @@ class StringView:
def skip_string(self, string):
strlen = len(string)
if self.buffer[self.index:self.index + strlen] == 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:]
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]
result = self.buffer[self.index : self.index + n]
self.previous = self.index
self.index += n
return result
@ -120,7 +121,7 @@ class StringView:
except IndexError:
break
self.previous = self.index
result = self.buffer[self.index:self.index + pos]
result = self.buffer[self.index : self.index + pos]
self.index += pos
return result
@ -129,7 +130,7 @@ class StringView:
if current is None:
return None
close_quote = _quotes.get(current)
close_quote = supported_quotes.get(current)
is_quoted = bool(close_quote)
if is_quoted:
result = []
@ -144,11 +145,11 @@ class StringView:
if is_quoted:
# unexpected EOF
raise ExpectedClosingQuoteError(close_quote)
return ''.join(result)
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 == '\\':
if current == "\\":
next_char = self.get()
if not next_char:
# string ends with \ and no character after it
@ -156,7 +157,7 @@ class StringView:
# if we're quoted then we're expecting a closing quote
raise ExpectedClosingQuoteError(close_quote)
# if we aren't then we just let it through
return ''.join(result)
return "".join(result)
if next_char in _escaped_quotes:
# escaped quote
@ -179,14 +180,13 @@ class StringView:
raise InvalidEndOfQuotedStringError(next_char)
# we're quoted so it's okay
return ''.join(result)
return "".join(result)
if current.isspace() and not is_quoted:
# end of word found
return ''.join(result)
return "".join(result)
result.append(current)
def __repr__(self):
return '<StringView pos: {0.index} prev: {0.previous} end: {0.end} eof: {0.eof}>'.format(self)
return f"<StringView pos: {self.index} prev: {self.previous} end: {self.end} eof: {self.eof}>"

View File

@ -22,35 +22,90 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import asyncio
import datetime
from typing import (
Any,
Awaitable,
Callable,
Generic,
List,
Optional,
Type,
TypeVar,
Union,
)
import aiohttp
import discord
import inspect
import logging
import sys
import traceback
from collections.abc import Sequence
from discord.backoff import ExponentialBackoff
from discord.utils import MISSING
log = logging.getLogger(__name__)
__all__ = ("loop",)
__all__ = (
'loop',
)
T = TypeVar("T")
_func = Callable[..., Awaitable[Any]]
LF = TypeVar("LF", bound=_func)
FT = TypeVar("FT", bound=_func)
ET = TypeVar("ET", bound=Callable[[Any, BaseException], Awaitable[Any]])
class Loop:
class SleepHandle:
__slots__ = ("future", "loop", "handle")
def __init__(self, dt: datetime.datetime, *, loop: asyncio.AbstractEventLoop) -> None:
self.loop = loop
self.future = future = loop.create_future()
relative_delta = discord.utils.compute_timedelta(dt)
self.handle = loop.call_later(relative_delta, future.set_result, True)
def recalculate(self, dt: datetime.datetime) -> None:
self.handle.cancel()
relative_delta = discord.utils.compute_timedelta(dt)
self.handle = self.loop.call_later(relative_delta, self.future.set_result, True)
def wait(self) -> asyncio.Future[Any]:
return self.future
def done(self) -> bool:
return self.future.done()
def cancel(self) -> None:
self.handle.cancel()
self.future.cancel()
class Loop(Generic[LF]):
"""A background task helper that abstracts the loop and reconnection logic for you.
The main interface to create this is through :func:`loop`.
"""
def __init__(self, coro, seconds, hours, minutes, count, reconnect, loop):
self.coro = coro
self.reconnect = reconnect
self.loop = loop
self.count = count
def __init__(
self,
coro: LF,
seconds: float,
hours: float,
minutes: float,
time: Union[datetime.time, Sequence[datetime.time]],
count: Optional[int],
reconnect: bool,
loop: asyncio.AbstractEventLoop,
) -> None:
self.coro: LF = coro
self.reconnect: bool = reconnect
self.loop: asyncio.AbstractEventLoop = loop
self.count: Optional[int] = count
self._current_loop = 0
self._task = None
self._handle: SleepHandle = MISSING
self._task: asyncio.Task[None] = MISSING
self._injected = None
self._valid_exception = (
OSError,
@ -67,18 +122,18 @@ class Loop:
self._stop_next_iteration = False
if self.count is not None and self.count <= 0:
raise ValueError('count must be greater than 0 or None.')
raise ValueError("count must be greater than 0 or None.")
self.change_interval(seconds=seconds, minutes=minutes, hours=hours)
self.change_interval(seconds=seconds, minutes=minutes, hours=hours, time=time)
self._last_iteration_failed = False
self._last_iteration = None
self._last_iteration: datetime.datetime = MISSING
self._next_iteration = None
if not inspect.iscoroutinefunction(self.coro):
raise TypeError('Expected coroutine function, not {0.__name__!r}.'.format(type(self.coro)))
raise TypeError(f"Expected coroutine function, not {type(self.coro).__name__!r}.")
async def _call_loop_function(self, name, *args, **kwargs):
coro = getattr(self, '_' + name)
async def _call_loop_function(self, name: str, *args: Any, **kwargs: Any) -> None:
coro = getattr(self, "_" + name)
if coro is None:
return
@ -87,14 +142,22 @@ class Loop:
else:
await coro(*args, **kwargs)
async def _loop(self, *args, **kwargs):
def _try_sleep_until(self, dt: datetime.datetime):
self._handle = SleepHandle(dt=dt, loop=self.loop)
return self._handle.wait()
async def _loop(self, *args: Any, **kwargs: Any) -> None:
backoff = ExponentialBackoff()
await self._call_loop_function('before_loop')
sleep_until = discord.utils.sleep_until
await self._call_loop_function("before_loop")
self._last_iteration_failed = False
self._next_iteration = datetime.datetime.now(datetime.timezone.utc)
if self._time is not MISSING:
# the time index should be prepared every time the internal loop is started
self._prepare_time_index()
self._next_iteration = self._get_next_sleep_time()
else:
self._next_iteration = datetime.datetime.now(datetime.timezone.utc)
try:
await asyncio.sleep(0) # allows canceling in before_loop
await self._try_sleep_until(self._next_iteration)
while True:
if not self._last_iteration_failed:
self._last_iteration = self._next_iteration
@ -102,42 +165,56 @@ class Loop:
try:
await self.coro(*args, **kwargs)
self._last_iteration_failed = False
now = datetime.datetime.now(datetime.timezone.utc)
if now > self._next_iteration:
self._next_iteration = now
except self._valid_exception:
self._last_iteration_failed = True
if not self.reconnect:
raise
await asyncio.sleep(backoff.delay())
else:
await self._try_sleep_until(self._next_iteration)
if self._stop_next_iteration:
return
now = datetime.datetime.now(datetime.timezone.utc)
if now > self._next_iteration:
self._next_iteration = now
if self._time is not MISSING:
self._prepare_time_index(now)
self._current_loop += 1
if self._current_loop == self.count:
break
await sleep_until(self._next_iteration)
except asyncio.CancelledError:
self._is_being_cancelled = True
raise
except Exception as exc:
self._has_failed = True
await self._call_loop_function('error', exc)
await self._call_loop_function("error", exc)
raise exc
finally:
await self._call_loop_function('after_loop')
await self._call_loop_function("after_loop")
self._handle.cancel()
self._is_being_cancelled = False
self._current_loop = 0
self._stop_next_iteration = False
self._has_failed = False
def __get__(self, obj, objtype):
def __get__(self, obj: T, objtype: Type[T]) -> Loop[LF]:
if obj is None:
return self
copy = Loop(self.coro, seconds=self.seconds, hours=self.hours, minutes=self.minutes,
count=self.count, reconnect=self.reconnect, loop=self.loop)
copy: Loop[LF] = Loop(
self.coro,
seconds=self._seconds,
hours=self._hours,
minutes=self._minutes,
time=self._time,
count=self.count,
reconnect=self.reconnect,
loop=self.loop,
)
copy._injected = obj
copy._before_loop = self._before_loop
copy._after_loop = self._after_loop
@ -146,23 +223,63 @@ class Loop:
return copy
@property
def current_loop(self):
def seconds(self) -> Optional[float]:
"""Optional[:class:`float`]: Read-only value for the number of seconds
between each iteration. ``None`` if an explicit ``time`` value was passed instead.
.. versionadded:: 2.0
"""
if self._seconds is not MISSING:
return self._seconds
@property
def minutes(self) -> Optional[float]:
"""Optional[:class:`float`]: Read-only value for the number of minutes
between each iteration. ``None`` if an explicit ``time`` value was passed instead.
.. versionadded:: 2.0
"""
if self._minutes is not MISSING:
return self._minutes
@property
def hours(self) -> Optional[float]:
"""Optional[:class:`float`]: Read-only value for the number of hours
between each iteration. ``None`` if an explicit ``time`` value was passed instead.
.. versionadded:: 2.0
"""
if self._hours is not MISSING:
return self._hours
@property
def time(self) -> Optional[List[datetime.time]]:
"""Optional[List[:class:`datetime.time`]]: Read-only list for the exact times this loop runs at.
``None`` if relative times were passed instead.
.. versionadded:: 2.0
"""
if self._time is not MISSING:
return self._time.copy()
@property
def current_loop(self) -> int:
""":class:`int`: The current iteration of the loop."""
return self._current_loop
@property
def next_iteration(self):
def next_iteration(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: When the next iteration of the loop will occur.
.. versionadded:: 1.3
"""
if self._task is None:
if self._task is MISSING:
return None
elif self._task and self._task.done() or self._stop_next_iteration:
return None
return self._next_iteration
async def __call__(self, *args, **kwargs):
async def __call__(self, *args: Any, **kwargs: Any) -> Any:
r"""|coro|
Calls the internal callback that the task holds.
@ -182,7 +299,7 @@ class Loop:
return await self.coro(*args, **kwargs)
def start(self, *args, **kwargs):
def start(self, *args: Any, **kwargs: Any) -> asyncio.Task[None]:
r"""Starts the internal task in the event loop.
Parameters
@ -203,19 +320,19 @@ class Loop:
The task that has been created.
"""
if self._task is not None and not self._task.done():
raise RuntimeError('Task is already launched and is not completed.')
if self._task is not MISSING and not self._task.done():
raise RuntimeError("Task is already launched and is not completed.")
if self._injected is not None:
args = (self._injected, *args)
if self.loop is None:
if self.loop is MISSING:
self.loop = asyncio.get_event_loop()
self._task = self.loop.create_task(self._loop(*args, **kwargs))
return self._task
def stop(self):
def stop(self) -> None:
r"""Gracefully stops the task from running.
Unlike :meth:`cancel`\, this allows the task to finish its
@ -233,18 +350,18 @@ class Loop:
.. versionadded:: 1.2
"""
if self._task and not self._task.done():
if self._task is not MISSING and not self._task.done():
self._stop_next_iteration = True
def _can_be_cancelled(self):
return not self._is_being_cancelled and self._task and not self._task.done()
def _can_be_cancelled(self) -> bool:
return bool(not self._is_being_cancelled and self._task and not self._task.done())
def cancel(self):
def cancel(self) -> None:
"""Cancels the internal task, if it is running."""
if self._can_be_cancelled():
self._task.cancel()
def restart(self, *args, **kwargs):
def restart(self, *args: Any, **kwargs: Any) -> None:
r"""A convenience method to restart the internal task.
.. note::
@ -255,12 +372,12 @@ class Loop:
Parameters
------------
\*args
The arguments to to use.
The arguments to use.
\*\*kwargs
The keyword arguments to use.
"""
def restart_when_over(fut, *, args=args, kwargs=kwargs):
def restart_when_over(fut: Any, *, args: Any = args, kwargs: Any = kwargs) -> None:
self._task.remove_done_callback(restart_when_over)
self.start(*args, **kwargs)
@ -268,7 +385,7 @@ class Loop:
self._task.add_done_callback(restart_when_over)
self._task.cancel()
def add_exception_type(self, *exceptions):
def add_exception_type(self, *exceptions: Type[BaseException]) -> None:
r"""Adds exception types to be handled during the reconnect logic.
By default the exception types handled are those handled by
@ -291,13 +408,13 @@ class Loop:
for exc in exceptions:
if not inspect.isclass(exc):
raise TypeError(f'{exc!r} must be a class.')
raise TypeError(f"{exc!r} must be a class.")
if not issubclass(exc, BaseException):
raise TypeError(f'{exc!r} must inherit from BaseException.')
raise TypeError(f"{exc!r} must inherit from BaseException.")
self._valid_exception = (*self._valid_exception, *exceptions)
def clear_exception_types(self):
def clear_exception_types(self) -> None:
"""Removes all exception types that are handled.
.. note::
@ -306,7 +423,7 @@ class Loop:
"""
self._valid_exception = tuple()
def remove_exception_type(self, *exceptions):
def remove_exception_type(self, *exceptions: Type[BaseException]) -> bool:
r"""Removes exception types from being handled during the reconnect logic.
Parameters
@ -323,34 +440,34 @@ class Loop:
self._valid_exception = tuple(x for x in self._valid_exception if x not in exceptions)
return len(self._valid_exception) == old_length - len(exceptions)
def get_task(self):
def get_task(self) -> Optional[asyncio.Task[None]]:
"""Optional[:class:`asyncio.Task`]: Fetches the internal task or ``None`` if there isn't one running."""
return self._task
return self._task if self._task is not MISSING else None
def is_being_cancelled(self):
def is_being_cancelled(self) -> bool:
"""Whether the task is being cancelled."""
return self._is_being_cancelled
def failed(self):
def failed(self) -> bool:
""":class:`bool`: Whether the internal task has failed.
.. versionadded:: 1.2
"""
return self._has_failed
def is_running(self):
def is_running(self) -> bool:
""":class:`bool`: Check if the task is currently running.
.. versionadded:: 1.4
"""
return not bool(self._task.done()) if self._task else False
return not bool(self._task.done()) if self._task is not MISSING else False
async def _error(self, *args):
exception = args[-1]
print(f'Unhandled exception in internal background task {self.coro.__name__!r}.', file=sys.stderr)
async def _error(self, *args: Any) -> None:
exception: Exception = args[-1]
print(f"Unhandled exception in internal background task {self.coro.__name__!r}.", file=sys.stderr)
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
def before_loop(self, coro):
def before_loop(self, coro: FT) -> FT:
"""A decorator that registers a coroutine to be called before the loop starts running.
This is useful if you want to wait for some bot state before the loop starts,
@ -370,12 +487,12 @@ class Loop:
"""
if not inspect.iscoroutinefunction(coro):
raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))
raise TypeError(f"Expected coroutine function, received {coro.__class__.__name__!r}.")
self._before_loop = coro
return coro
def after_loop(self, coro):
def after_loop(self, coro: FT) -> FT:
"""A decorator that register a coroutine to be called after the loop finished running.
The coroutine must take no arguments (except ``self`` in a class context).
@ -398,12 +515,12 @@ class Loop:
"""
if not inspect.iscoroutinefunction(coro):
raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))
raise TypeError(f"Expected coroutine function, received {coro.__class__.__name__!r}.")
self._after_loop = coro
return coro
def error(self, coro):
def error(self, coro: ET) -> ET:
"""A decorator that registers a coroutine to be called if the task encounters an unhandled exception.
The coroutine must take only one argument the exception raised (except ``self`` in a class context).
@ -424,22 +541,90 @@ class Loop:
The function was not a coroutine.
"""
if not inspect.iscoroutinefunction(coro):
raise TypeError('Expected coroutine function, received {0.__name__!r}.'.format(type(coro)))
raise TypeError(f"Expected coroutine function, received {coro.__class__.__name__!r}.")
self._error = coro
self._error = coro # type: ignore
return coro
def _get_next_sleep_time(self):
return self._last_iteration + datetime.timedelta(seconds=self._sleep)
def _get_next_sleep_time(self) -> datetime.datetime:
if self._sleep is not MISSING:
return self._last_iteration + datetime.timedelta(seconds=self._sleep)
def change_interval(self, *, seconds=0, minutes=0, hours=0):
if self._time_index >= len(self._time):
self._time_index = 0
if self._current_loop == 0:
# if we're at the last index on the first iteration, we need to sleep until tomorrow
return datetime.datetime.combine(
datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1), self._time[0]
)
next_time = self._time[self._time_index]
if self._current_loop == 0:
self._time_index += 1
return datetime.datetime.combine(datetime.datetime.now(datetime.timezone.utc), next_time)
next_date = self._last_iteration
if self._time_index == 0:
# we can assume that the earliest time should be scheduled for "tomorrow"
next_date += datetime.timedelta(days=1)
self._time_index += 1
return datetime.datetime.combine(next_date, next_time)
def _prepare_time_index(self, now: datetime.datetime = MISSING) -> None:
# now kwarg should be a datetime.datetime representing the time "now"
# to calculate the next time index from
# pre-condition: self._time is set
time_now = (
now if now is not MISSING else datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0)
).timetz()
for idx, time in enumerate(self._time):
if time >= time_now:
self._time_index = idx
break
else:
self._time_index = 0
def _get_time_parameter(
self,
time: Union[datetime.time, Sequence[datetime.time]],
*,
dt: Type[datetime.time] = datetime.time,
utc: datetime.timezone = datetime.timezone.utc,
) -> List[datetime.time]:
if isinstance(time, dt):
inner = time if time.tzinfo is not None else time.replace(tzinfo=utc)
return [inner]
if not isinstance(time, Sequence):
raise TypeError(
f"Expected datetime.time or a sequence of datetime.time for ``time``, received {type(time)!r} instead."
)
if not time:
raise ValueError("time parameter must not be an empty sequence.")
ret: List[datetime.time] = []
for index, t in enumerate(time):
if not isinstance(t, dt):
raise TypeError(
f"Expected a sequence of {dt!r} for ``time``, received {type(t).__name__!r} at index {index} instead."
)
ret.append(t if t.tzinfo is not None else t.replace(tzinfo=utc))
ret = sorted(set(ret)) # de-dupe and sort times
return ret
def change_interval(
self,
*,
seconds: float = 0,
minutes: float = 0,
hours: float = 0,
time: Union[datetime.time, Sequence[datetime.time]] = MISSING,
) -> None:
"""Changes the interval for the sleep time.
.. note::
This only applies on the next loop iteration. If it is desirable for the change of interval
to be applied right away, cancel the task with :meth:`cancel`.
.. versionadded:: 1.2
Parameters
@ -450,23 +635,66 @@ class Loop:
The number of minutes between every iteration.
hours: :class:`float`
The number of hours between every iteration.
time: Union[:class:`datetime.time`, Sequence[:class:`datetime.time`]]
The exact times to run this loop at. Either a non-empty list or a single
value of :class:`datetime.time` should be passed.
This cannot be used in conjunction with the relative time parameters.
.. versionadded:: 2.0
.. note::
Duplicate times will be ignored, and only run once.
Raises
-------
ValueError
An invalid value was given.
TypeError
An invalid value for the ``time`` parameter was passed, or the
``time`` parameter was passed in conjunction with relative time parameters.
"""
sleep = seconds + (minutes * 60.0) + (hours * 3600.0)
if sleep < 0:
raise ValueError('Total number of seconds cannot be less than zero.')
if time is MISSING:
seconds = seconds or 0
minutes = minutes or 0
hours = hours or 0
sleep = seconds + (minutes * 60.0) + (hours * 3600.0)
if sleep < 0:
raise ValueError("Total number of seconds cannot be less than zero.")
self._sleep = sleep
self.seconds = seconds
self.hours = hours
self.minutes = minutes
self._sleep = sleep
self._seconds = float(seconds)
self._hours = float(hours)
self._minutes = float(minutes)
self._time: List[datetime.time] = MISSING
else:
if any((seconds, minutes, hours)):
raise TypeError("Cannot mix explicit time with relative time")
self._time = self._get_time_parameter(time)
self._sleep = self._seconds = self._minutes = self._hours = MISSING
def loop(*, seconds=0, minutes=0, hours=0, count=None, reconnect=True, loop=None):
if self.is_running():
if self._time is not MISSING:
# prepare the next time index starting from after the last iteration
self._prepare_time_index(now=self._last_iteration)
self._next_iteration = self._get_next_sleep_time()
if not self._handle.done():
# the loop is sleeping, recalculate based on new interval
self._handle.recalculate(self._next_iteration)
def loop(
*,
seconds: float = MISSING,
minutes: float = MISSING,
hours: float = MISSING,
time: Union[datetime.time, Sequence[datetime.time]] = MISSING,
count: Optional[int] = None,
reconnect: bool = True,
loop: asyncio.AbstractEventLoop = MISSING,
) -> Callable[[LF], Loop[LF]]:
"""A decorator that schedules a task in the background for you with
optional reconnect logic. The decorator returns a :class:`Loop`.
@ -478,6 +706,19 @@ def loop(*, seconds=0, minutes=0, hours=0, count=None, reconnect=True, loop=None
The number of minutes between every iteration.
hours: :class:`float`
The number of hours between every iteration.
time: Union[:class:`datetime.time`, Sequence[:class:`datetime.time`]]
The exact times to run this loop at. Either a non-empty list or a single
value of :class:`datetime.time` should be passed. Timezones are supported.
If no timezone is given for the times, it is assumed to represent UTC time.
This cannot be used in conjunction with the relative time parameters.
.. note::
Duplicate times will be ignored, and only run once.
.. versionadded:: 2.0
count: Optional[:class:`int`]
The number of loops to do, ``None`` if it should be an
infinite loop.
@ -494,16 +735,20 @@ def loop(*, seconds=0, minutes=0, hours=0, count=None, reconnect=True, loop=None
ValueError
An invalid value was given.
TypeError
The function was not a coroutine.
The function was not a coroutine, an invalid value for the ``time`` parameter was passed,
or ``time`` parameter was passed in conjunction with relative time parameters.
"""
def decorator(func):
kwargs = {
'seconds': seconds,
'minutes': minutes,
'hours': hours,
'count': count,
'reconnect': reconnect,
'loop': loop
}
return Loop(func, **kwargs)
def decorator(func: LF) -> Loop[LF]:
return Loop[LF](
func,
seconds=seconds,
minutes=minutes,
hours=hours,
count=count,
time=time,
reconnect=reconnect,
loop=loop,
)
return decorator

View File

@ -22,12 +22,14 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import os.path
from __future__ import annotations
from typing import Optional, TYPE_CHECKING, Union
import os
import io
__all__ = (
'File',
)
__all__ = ("File",)
class File:
r"""A parameter object used for :meth:`abc.Messageable.send`
@ -40,7 +42,7 @@ class File:
Attributes
-----------
fp: Union[:class:`str`, :class:`io.BufferedIOBase`]
fp: Union[:class:`os.PathLike`, :class:`io.BufferedIOBase`]
A file-like object opened in binary mode and read mode
or a filename representing a file in the hard drive to
open.
@ -60,19 +62,28 @@ class File:
Whether the attachment is a spoiler.
"""
__slots__ = ('fp', 'filename', 'spoiler', '_original_pos', '_owner', '_closer')
__slots__ = ("fp", "filename", "spoiler", "_original_pos", "_owner", "_closer")
def __init__(self, fp, filename=None, *, spoiler=False):
self.fp = fp
if TYPE_CHECKING:
fp: io.BufferedIOBase
filename: Optional[str]
spoiler: bool
def __init__(
self,
fp: Union[str, bytes, os.PathLike, io.BufferedIOBase],
filename: Optional[str] = None,
*,
spoiler: bool = False,
):
if isinstance(fp, io.IOBase):
if not (fp.seekable() and fp.readable()):
raise ValueError(f'File buffer {fp!r} must be seekable and readable')
raise ValueError(f"File buffer {fp!r} must be seekable and readable")
self.fp = fp
self._original_pos = fp.tell()
self._owner = False
else:
self.fp = open(fp, 'rb')
self.fp = open(fp, "rb")
self._original_pos = 0
self._owner = True
@ -87,16 +98,16 @@ class File:
if isinstance(fp, str):
_, self.filename = os.path.split(fp)
else:
self.filename = getattr(fp, 'name', None)
self.filename = getattr(fp, "name", None)
else:
self.filename = filename
if spoiler and self.filename is not None and not self.filename.startswith('SPOILER_'):
self.filename = 'SPOILER_' + self.filename
if spoiler and self.filename is not None and not self.filename.startswith("SPOILER_"):
self.filename = "SPOILER_" + self.filename
self.spoiler = spoiler or (self.filename is not None and self.filename.startswith('SPOILER_'))
self.spoiler = spoiler or (self.filename is not None and self.filename.startswith("SPOILER_"))
def reset(self, *, seek=True):
def reset(self, *, seek: Union[int, bool] = True) -> None:
# The `seek` parameter is needed because
# the retry-loop is iterated over multiple times
# starting from 0, as an implementation quirk
@ -108,7 +119,7 @@ class File:
if seek:
self.fp.seek(self._original_pos)
def close(self):
def close(self) -> None:
self.fp.close = self._closer
if self._owner:
self._closer()

View File

@ -29,17 +29,19 @@ from typing import Any, Callable, ClassVar, Dict, Generic, Iterator, List, Optio
from .enums import UserFlags
__all__ = (
'SystemChannelFlags',
'MessageFlags',
'PublicUserFlags',
'Intents',
'MemberCacheFlags',
"SystemChannelFlags",
"MessageFlags",
"PublicUserFlags",
"Intents",
"MemberCacheFlags",
"ApplicationFlags",
)
FV = TypeVar('FV', bound='flag_value')
BF = TypeVar('BF', bound='BaseFlags')
FV = TypeVar("FV", bound="flag_value")
BF = TypeVar("BF", bound="BaseFlags")
class flag_value(Generic[BF]):
class flag_value:
def __init__(self, func: Callable[[Any], int]):
self.flag = func(None)
self.__doc__ = func.__doc__
@ -61,18 +63,22 @@ class flag_value(Generic[BF]):
instance._set_flag(self.flag, value)
def __repr__(self):
return f'<flag_value flag={self.flag!r}>'
return f"<flag_value flag={self.flag!r}>"
class alias_flag_value(flag_value):
pass
def fill_with_flags(*, inverted: bool = False):
def decorator(cls: Type[BF]):
# fmt: off
cls.VALID_FLAGS = {
name: value.flag
for name, value in cls.__dict__.items()
if isinstance(value, flag_value)
}
# fmt: on
if inverted:
max_bits = max(cls.VALID_FLAGS.values()).bit_length()
@ -81,8 +87,10 @@ def fill_with_flags(*, inverted: bool = False):
cls.DEFAULT_VALUE = 0
return cls
return decorator
# n.b. flags must inherit from this and use the decorator above
class BaseFlags:
VALID_FLAGS: ClassVar[Dict[str, int]]
@ -90,13 +98,13 @@ class BaseFlags:
value: int
__slots__ = ('value',)
__slots__ = ("value",)
def __init__(self, **kwargs: bool):
self.value = self.DEFAULT_VALUE
for key, value in kwargs.items():
if key not in self.VALID_FLAGS:
raise TypeError(f'{key!r} is not a valid flag name.')
raise TypeError(f"{key!r} is not a valid flag name.")
setattr(self, key, value)
@classmethod
@ -115,7 +123,7 @@ class BaseFlags:
return hash(self.value)
def __repr__(self) -> str:
return f'<{self.__class__.__name__} value={self.value}>'
return f"<{self.__class__.__name__} value={self.value}>"
def __iter__(self) -> Iterator[Tuple[str, bool]]:
for name, value in self.__class__.__dict__.items():
@ -134,7 +142,8 @@ class BaseFlags:
elif toggle is False:
self.value &= ~o
else:
raise TypeError(f'Value to set for {self.__class__.__name__} must be a bool.')
raise TypeError(f"Value to set for {self.__class__.__name__} must be a bool.")
@fill_with_flags(inverted=True)
class SystemChannelFlags(BaseFlags):
@ -187,7 +196,7 @@ class SystemChannelFlags(BaseFlags):
elif toggle is False:
self.value |= o
else:
raise TypeError('Value to set for SystemChannelFlags must be a bool.')
raise TypeError("Value to set for SystemChannelFlags must be a bool.")
@flag_value
def join_notifications(self):
@ -196,9 +205,17 @@ class SystemChannelFlags(BaseFlags):
@flag_value
def premium_subscriptions(self):
""":class:`bool`: Returns ``True`` if the system channel is used for Nitro boosting notifications."""
""":class:`bool`: Returns ``True`` if the system channel is used for "Nitro boosting" notifications."""
return 2
@flag_value
def guild_reminder_notifications(self):
""":class:`bool`: Returns ``True`` if the system channel is used for server setup helpful tips notifications.
.. versionadded:: 2.0
"""
return 4
@fill_with_flags()
class MessageFlags(BaseFlags):
@ -262,6 +279,23 @@ class MessageFlags(BaseFlags):
"""
return 16
@flag_value
def has_thread(self):
""":class:`bool`: Returns ``True`` if the source message is associated with a thread.
.. versionadded:: 2.0
"""
return 32
@flag_value
def ephemeral(self):
""":class:`bool`: Returns ``True`` if the source message is ephemeral.
.. versionadded:: 2.0
"""
return 64
@fill_with_flags()
class PublicUserFlags(BaseFlags):
r"""Wraps up the Discord User Public flags.
@ -368,6 +402,14 @@ class PublicUserFlags(BaseFlags):
"""
return UserFlags.verified_bot_developer.value
@flag_value
def discord_certified_moderator(self):
""":class:`bool`: Returns ``True`` if the user is a Discord Certified Moderator.
.. versionadded:: 2.0
"""
return UserFlags.discord_certified_moderator.value
def all(self) -> List[UserFlags]:
"""List[:class:`UserFlags`]: Returns all public flags the user has."""
return [public_flag for public_flag in UserFlags if self._has_flag(public_flag.value)]
@ -419,7 +461,7 @@ class Intents(BaseFlags):
self.value = self.DEFAULT_VALUE
for key, value in kwargs.items():
if key not in self.VALID_FLAGS:
raise TypeError(f'{key!r} is not a valid flag name.')
raise TypeError(f"{key!r} is not a valid flag name.")
setattr(self, key, value)
@classmethod
@ -438,16 +480,6 @@ class Intents(BaseFlags):
self.value = self.DEFAULT_VALUE
return self
@classmethod
def default(cls: Type[Intents]) -> Intents:
"""A factory method that creates a :class:`Intents` with everything enabled
except :attr:`presences` and :attr:`members`.
"""
self = cls.all()
self.presences = False
self.members = False
return self
@flag_value
def guilds(self):
""":class:`bool`: Whether guild related events are enabled.
@ -482,12 +514,13 @@ class Intents(BaseFlags):
- :func:`on_member_join`
- :func:`on_member_remove`
- :func:`on_member_update` (nickname, roles)
- :func:`on_member_update`
- :func:`on_user_update`
This also corresponds to the following attributes and classes in terms of cache:
- :meth:`Client.get_all_members`
- :meth:`Client.get_user`
- :meth:`Guild.chunk`
- :meth:`Guild.fetch_members`
- :meth:`Guild.get_member`
@ -496,7 +529,7 @@ class Intents(BaseFlags):
- :attr:`Member.nick`
- :attr:`Member.premium_since`
- :attr:`User.name`
- :attr:`User.avatar` (:attr:`User.avatar_url` and :meth:`User.avatar_url_as`)
- :attr:`User.avatar`
- :attr:`User.discriminator`
For more information go to the :ref:`member intent documentation <need_members_intent>`.
@ -523,18 +556,34 @@ class Intents(BaseFlags):
@flag_value
def emojis(self):
""":class:`bool`: Whether guild emoji related events are enabled.
""":class:`bool`: Alias of :attr:`.emojis_and_stickers`.
.. versionchanged:: 2.0
Changed to an alias.
"""
return 1 << 3
@alias_flag_value
def emojis_and_stickers(self):
""":class:`bool`: Whether guild emoji and sticker related events are enabled.
.. versionadded:: 2.0
This corresponds to the following events:
- :func:`on_guild_emojis_update`
- :func:`on_guild_stickers_update`
This also corresponds to the following attributes and classes in terms of cache:
- :class:`Emoji`
- :class:`GuildSticker`
- :meth:`Client.get_emoji`
- :meth:`Client.get_sticker`
- :meth:`Client.emojis`
- :meth:`Client.stickers`
- :attr:`Guild.emojis`
- :attr:`Guild.stickers`
"""
return 1 << 3
@ -545,6 +594,9 @@ class Intents(BaseFlags):
This corresponds to the following events:
- :func:`on_guild_integrations_update`
- :func:`on_integration_create`
- :func:`on_integration_update`
- :func:`on_raw_integration_delete`
This does not correspond to any attributes or classes in the library in terms of cache.
"""
@ -588,6 +640,10 @@ class Intents(BaseFlags):
- :attr:`VoiceChannel.members`
- :attr:`VoiceChannel.voice_states`
- :attr:`Member.voice`
.. note::
This intent is required to connect to voice.
"""
return 1 << 7
@ -597,7 +653,7 @@ class Intents(BaseFlags):
This corresponds to the following events:
- :func:`on_member_update` (activities, status)
- :func:`on_presence_update`
This also corresponds to the following attributes and classes in terms of cache:
@ -627,7 +683,6 @@ class Intents(BaseFlags):
- :func:`on_message_delete` (both guilds and DMs)
- :func:`on_raw_message_delete` (both guilds and DMs)
- :func:`on_raw_message_edit` (both guilds and DMs)
- :func:`on_private_channel_create`
This also corresponds to the following attributes and classes in terms of cache:
@ -682,7 +737,6 @@ class Intents(BaseFlags):
- :func:`on_message_delete` (only for DMs)
- :func:`on_raw_message_delete` (only for DMs)
- :func:`on_raw_message_edit` (only for DMs)
- :func:`on_private_channel_create`
This also corresponds to the following attributes and classes in terms of cache:
@ -802,6 +856,7 @@ class Intents(BaseFlags):
"""
return 1 << 14
@fill_with_flags()
class MemberCacheFlags(BaseFlags):
"""Controls the library's cache policy when it comes to members.
@ -852,7 +907,7 @@ class MemberCacheFlags(BaseFlags):
self.value = (1 << bits) - 1
for key, value in kwargs.items():
if key not in self.VALID_FLAGS:
raise TypeError(f'{key!r} is not a valid flag name.')
raise TypeError(f"{key!r} is not a valid flag name.")
setattr(self, key, value)
@classmethod
@ -875,17 +930,6 @@ class MemberCacheFlags(BaseFlags):
def _empty(self):
return self.value == self.DEFAULT_VALUE
@flag_value
def online(self):
""":class:`bool`: Whether to cache members with a status.
For example, members that are part of the initial ``GUILD_CREATE``
or become online at a later point. This requires :attr:`Intents.presences`.
Members that go offline are no longer cached.
"""
return 1
@flag_value
def voice(self):
""":class:`bool`: Whether to cache members that are in voice.
@ -894,7 +938,7 @@ class MemberCacheFlags(BaseFlags):
Members that leave voice are no longer cached.
"""
return 2
return 1
@flag_value
def joined(self):
@ -905,7 +949,7 @@ class MemberCacheFlags(BaseFlags):
Members that leave the guild are no longer cached.
"""
return 4
return 2
@classmethod
def from_intents(cls: Type[MemberCacheFlags], intents: Intents) -> MemberCacheFlags:
@ -926,35 +970,89 @@ class MemberCacheFlags(BaseFlags):
self = cls.none()
if intents.members:
self.joined = True
if intents.presences:
self.online = True
if intents.voice_states:
self.voice = True
if not self.joined and self.online and self.voice:
self.voice = False
return self
def _verify_intents(self, intents: Intents):
if self.online and not intents.presences:
raise ValueError('MemberCacheFlags.online requires Intents.presences enabled')
if self.voice and not intents.voice_states:
raise ValueError('MemberCacheFlags.voice requires Intents.voice_states')
raise ValueError("MemberCacheFlags.voice requires Intents.voice_states")
if self.joined and not intents.members:
raise ValueError('MemberCacheFlags.joined requires Intents.members')
if not self.joined and self.voice and self.online:
msg = 'Setting both MemberCacheFlags.voice and MemberCacheFlags.online requires MemberCacheFlags.joined ' \
'to properly evict members from the cache.'
raise ValueError(msg)
raise ValueError("MemberCacheFlags.joined requires Intents.members")
@property
def _voice_only(self):
return self.value == 2
@property
def _online_only(self):
return self.value == 1
@fill_with_flags()
class ApplicationFlags(BaseFlags):
r"""Wraps up the Discord Application flags.
.. container:: operations
.. describe:: x == y
Checks if two ApplicationFlags are equal.
.. describe:: x != y
Checks if two ApplicationFlags are not equal.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. versionadded:: 2.0
Attributes
-----------
value: :class:`int`
The raw value. You should query flags via the properties
rather than using this raw value.
"""
@flag_value
def gateway_presence(self):
""":class:`bool`: Returns ``True`` if the application is verified and is allowed to
receive presence information over the gateway.
"""
return 1 << 12
@flag_value
def gateway_presence_limited(self):
""":class:`bool`: Returns ``True`` if the application is allowed to receive limited
presence information over the gateway.
"""
return 1 << 13
@flag_value
def gateway_guild_members(self):
""":class:`bool`: Returns ``True`` if the application is verified and is allowed to
receive guild members information over the gateway.
"""
return 1 << 14
@flag_value
def gateway_guild_members_limited(self):
""":class:`bool`: Returns ``True`` if the application is allowed to receive limited
guild members information over the gateway.
"""
return 1 << 15
@flag_value
def verification_pending_guild_limit(self):
""":class:`bool`: Returns ``True`` if the application is currently pending verification
and has hit the guild limit.
"""
return 1 << 16
@flag_value
def embedded(self):
""":class:`bool`: Returns ``True`` if the application is embedded within the Discord client."""
return 1 << 17

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -22,17 +22,36 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import datetime
from .utils import _get_as_snowflake, get, parse_time
from typing import Any, Dict, Optional, TYPE_CHECKING, overload, Type, Tuple
from .utils import _get_as_snowflake, parse_time, MISSING
from .user import User
from .errors import InvalidArgument
from .enums import try_enum, ExpireBehaviour
__all__ = (
'IntegrationAccount',
'Integration',
"IntegrationAccount",
"IntegrationApplication",
"Integration",
"StreamIntegration",
"BotIntegration",
)
if TYPE_CHECKING:
from .types.integration import (
IntegrationAccount as IntegrationAccountPayload,
Integration as IntegrationPayload,
StreamIntegration as StreamIntegrationPayload,
BotIntegration as BotIntegrationPayload,
IntegrationType,
IntegrationApplication as IntegrationApplicationPayload,
)
from .guild import Guild
from .role import Role
class IntegrationAccount:
"""Represents an integration account.
@ -40,20 +59,21 @@ class IntegrationAccount:
Attributes
-----------
id: :class:`int`
id: :class:`str`
The account ID.
name: :class:`str`
The account name.
"""
__slots__ = ('id', 'name')
__slots__ = ("id", "name")
def __init__(self, **kwargs):
self.id = kwargs.pop('id')
self.name = kwargs.pop('name')
def __init__(self, data: IntegrationAccountPayload) -> None:
self.id: str = data["id"]
self.name: str = data["name"]
def __repr__(self) -> str:
return f"<IntegrationAccount id={self.id} name={self.name!r}>"
def __repr__(self):
return '<IntegrationAccount id={0.id} name={0.name!r}>'.format(self)
class Integration:
"""Represents a guild integration.
@ -62,6 +82,83 @@ class Integration:
Attributes
-----------
id: :class:`int`
The integration ID.
name: :class:`str`
The integration name.
guild: :class:`Guild`
The guild of the integration.
type: :class:`str`
The integration type (i.e. Twitch).
enabled: :class:`bool`
Whether the integration is currently enabled.
account: :class:`IntegrationAccount`
The account linked to this integration.
user: :class:`User`
The user that added this integration.
"""
__slots__ = (
"guild",
"id",
"_state",
"type",
"name",
"account",
"user",
"enabled",
)
def __init__(self, *, data: IntegrationPayload, guild: Guild) -> None:
self.guild = guild
self._state = guild._state
self._from_data(data)
def __repr__(self):
return f"<{self.__class__.__name__} id={self.id} name={self.name!r}>"
def _from_data(self, data: IntegrationPayload) -> None:
self.id: int = int(data["id"])
self.type: IntegrationType = data["type"]
self.name: str = data["name"]
self.account: IntegrationAccount = IntegrationAccount(data["account"])
user = data.get("user")
self.user = User(state=self._state, data=user) if user else None
self.enabled: bool = data["enabled"]
async def delete(self, *, reason: Optional[str] = None) -> None:
"""|coro|
Deletes the integration.
You must have the :attr:`~Permissions.manage_guild` permission to
do this.
Parameters
-----------
reason: :class:`str`
The reason the integration was deleted. Shows up on the audit log.
.. versionadded:: 2.0
Raises
-------
Forbidden
You do not have permission to delete the integration.
HTTPException
Deleting the integration failed.
"""
await self._state.http.delete_integration(self.guild.id, self.id, reason=reason)
class StreamIntegration(Integration):
"""Represents a stream integration for Twitch or YouTube.
.. versionadded:: 2.0
Attributes
----------
id: :class:`int`
The integration ID.
name: :class:`str`
@ -74,8 +171,6 @@ class Integration:
Whether the integration is currently enabled.
syncing: :class:`bool`
Where the integration is currently syncing.
role: :class:`Role`
The role which the integration uses for subscribers.
enable_emoticons: Optional[:class:`bool`]
Whether emoticons should be synced for this integration (currently twitch only).
expire_behaviour: :class:`ExpireBehaviour`
@ -90,37 +185,45 @@ class Integration:
An aware UTC datetime representing when the integration was last synced.
"""
__slots__ = ('id', '_state', 'guild', 'name', 'enabled', 'type',
'syncing', 'role', 'expire_behaviour', 'expire_behavior',
'expire_grace_period', 'synced_at', 'user', 'account',
'enable_emoticons', '_role_id')
__slots__ = (
"revoked",
"expire_behaviour",
"expire_grace_period",
"synced_at",
"_role_id",
"syncing",
"enable_emoticons",
"subscriber_count",
)
def __init__(self, *, data, guild):
self.guild = guild
self._state = guild._state
self._from_data(data)
def _from_data(self, data: StreamIntegrationPayload) -> None:
super()._from_data(data)
self.revoked: bool = data["revoked"]
self.expire_behaviour: ExpireBehaviour = try_enum(ExpireBehaviour, data["expire_behavior"])
self.expire_grace_period: int = data["expire_grace_period"]
self.synced_at: datetime.datetime = parse_time(data["synced_at"])
self._role_id: Optional[int] = _get_as_snowflake(data, "role_id")
self.syncing: bool = data["syncing"]
self.enable_emoticons: bool = data["enable_emoticons"]
self.subscriber_count: int = data["subscriber_count"]
def __repr__(self):
return '<Integration id={0.id} name={0.name!r} type={0.type!r}>'.format(self)
@property
def expire_behavior(self) -> ExpireBehaviour:
""":class:`ExpireBehaviour`: An alias for :attr:`expire_behaviour`."""
return self.expire_behaviour
def _from_data(self, integ):
self.id = _get_as_snowflake(integ, 'id')
self.name = integ['name']
self.type = integ['type']
self.enabled = integ['enabled']
self.syncing = integ['syncing']
self._role_id = _get_as_snowflake(integ, 'role_id')
self.role = get(self.guild.roles, id=self._role_id)
self.enable_emoticons = integ.get('enable_emoticons')
self.expire_behaviour = try_enum(ExpireBehaviour, integ['expire_behavior'])
self.expire_behavior = self.expire_behaviour
self.expire_grace_period = integ['expire_grace_period']
self.synced_at = parse_time(integ['synced_at'])
@property
def role(self) -> Optional[Role]:
"""Optional[:class:`Role`] The role which the integration uses for subscribers."""
return self.guild.get_role(self._role_id) # type: ignore
self.user = User(state=self._state, data=integ['user'])
self.account = IntegrationAccount(**integ['account'])
async def edit(self, **fields):
async def edit(
self,
*,
expire_behaviour: ExpireBehaviour = MISSING,
expire_grace_period: int = MISSING,
enable_emoticons: bool = MISSING,
) -> None:
"""|coro|
Edits the integration.
@ -146,34 +249,24 @@ class Integration:
InvalidArgument
``expire_behaviour`` did not receive a :class:`ExpireBehaviour`.
"""
try:
expire_behaviour = fields['expire_behaviour']
except KeyError:
expire_behaviour = fields.get('expire_behavior', self.expire_behaviour)
payload: Dict[str, Any] = {}
if expire_behaviour is not MISSING:
if not isinstance(expire_behaviour, ExpireBehaviour):
raise InvalidArgument("expire_behaviour field must be of type ExpireBehaviour")
if not isinstance(expire_behaviour, ExpireBehaviour):
raise InvalidArgument('expire_behaviour field must be of type ExpireBehaviour')
payload["expire_behavior"] = expire_behaviour.value
expire_grace_period = fields.get('expire_grace_period', self.expire_grace_period)
if expire_grace_period is not MISSING:
payload["expire_grace_period"] = expire_grace_period
payload = {
'expire_behavior': expire_behaviour.value,
'expire_grace_period': expire_grace_period,
}
enable_emoticons = fields.get('enable_emoticons')
if enable_emoticons is not None:
payload['enable_emoticons'] = enable_emoticons
if enable_emoticons is not MISSING:
payload["enable_emoticons"] = enable_emoticons
# This endpoint is undocumented.
# Unsure if it returns the data or not as a result
await self._state.http.edit_integration(self.guild.id, self.id, **payload)
self.expire_behaviour = expire_behaviour
self.expire_behavior = self.expire_behaviour
self.expire_grace_period = expire_grace_period
self.enable_emoticons = enable_emoticons
async def sync(self):
async def sync(self) -> None:
"""|coro|
Syncs the integration.
@ -191,19 +284,83 @@ class Integration:
await self._state.http.sync_integration(self.guild.id, self.id)
self.synced_at = datetime.datetime.now(datetime.timezone.utc)
async def delete(self):
"""|coro|
Deletes the integration.
class IntegrationApplication:
"""Represents an application for a bot integration.
You must have the :attr:`~Permissions.manage_guild` permission to
do this.
.. versionadded:: 2.0
Raises
-------
Forbidden
You do not have permission to delete the integration.
HTTPException
Deleting the integration failed.
"""
await self._state.http.delete_integration(self.guild.id, self.id)
Attributes
----------
id: :class:`int`
The ID for this application.
name: :class:`str`
The application's name.
icon: Optional[:class:`str`]
The application's icon hash.
description: :class:`str`
The application's description. Can be an empty string.
summary: :class:`str`
The summary of the application. Can be an empty string.
user: Optional[:class:`User`]
The bot user on this application.
"""
__slots__ = (
"id",
"name",
"icon",
"description",
"summary",
"user",
)
def __init__(self, *, data: IntegrationApplicationPayload, state):
self.id: int = int(data["id"])
self.name: str = data["name"]
self.icon: Optional[str] = data["icon"]
self.description: str = data["description"]
self.summary: str = data["summary"]
user = data.get("bot")
self.user: Optional[User] = User(state=state, data=user) if user else None
class BotIntegration(Integration):
"""Represents a bot integration on discord.
.. versionadded:: 2.0
Attributes
----------
id: :class:`int`
The integration ID.
name: :class:`str`
The integration name.
guild: :class:`Guild`
The guild of the integration.
type: :class:`str`
The integration type (i.e. Twitch).
enabled: :class:`bool`
Whether the integration is currently enabled.
user: :class:`User`
The user that added this integration.
account: :class:`IntegrationAccount`
The integration account information.
application: :class:`IntegrationApplication`
The application tied to this integration.
"""
__slots__ = ("application",)
def _from_data(self, data: BotIntegrationPayload) -> None:
super()._from_data(data)
self.application = IntegrationApplication(data=data["application"], state=self._state)
def _integration_factory(value: str) -> Tuple[Type[Integration], str]:
if value == "discord":
return BotIntegration, value
elif value in ("twitch", "youtube"):
return StreamIntegration, value
else:
return Integration, value

View File

@ -25,20 +25,55 @@ DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Union
import asyncio
from . import utils
from .enums import try_enum, InteractionType
from .enums import try_enum, InteractionType, InteractionResponseType
from .errors import InteractionResponded, HTTPException, ClientException
from .channel import PartialMessageable, ChannelType
from .user import User
from .member import Member
from .message import Message, Attachment
from .object import Object
from .permissions import Permissions
from .webhook.async_ import async_context, Webhook, handle_message_parameters
__all__ = (
'Interaction',
"Interaction",
"InteractionMessage",
"InteractionResponse",
)
if TYPE_CHECKING:
from datetime import datetime
from .types.interactions import (
Interaction as InteractionPayload,
ApplicationCommandOptionChoice,
InteractionData,
)
from .guild import Guild
from .state import ConnectionState
from .file import File
from .mentions import AllowedMentions
from aiohttp import ClientSession
from .embeds import Embed
from .ui.view import View
from .channel import TextChannel, CategoryChannel, StoreChannel, PartialMessageable
from .threads import Thread
InteractionChannel = Union[TextChannel, CategoryChannel, StoreChannel, Thread, PartialMessageable]
MISSING: Any = utils.MISSING
class Interaction:
"""Represents a Discord interaction.
An interaction happens when a user does an action that needs to
be notified. Current examples are slash commands but future examples
include forms and buttons.
be notified. Current examples are slash commands and components.
.. versionadded:: 2.0
@ -56,49 +91,716 @@ class Interaction:
The application ID that the interaction was for.
user: Optional[Union[:class:`User`, :class:`Member`]]
The user or member that sent the interaction.
message: Optional[:class:`Message`]
The message that sent this interaction.
token: :class:`str`
The token to continue the interaction. These are valid
for 15 minutes.
data: :class:`dict`
The raw interaction data.
"""
__slots__ = (
'id',
'type',
'guild_id',
'channel_id',
'data',
'application_id',
'user',
'token',
'version',
'_state',
__slots__: Tuple[str, ...] = (
"id",
"type",
"guild_id",
"channel_id",
"data",
"application_id",
"message",
"user",
"token",
"version",
"_permissions",
"_state",
"_session",
"_original_message",
"_cs_response",
"_cs_followup",
"_cs_channel",
)
def __init__(self, *, data, state=None):
self._state = state
def __init__(self, *, data: InteractionPayload, state: ConnectionState):
self._state: ConnectionState = state
self._session: ClientSession = state.http._HTTPClient__session
self._original_message: Optional[InteractionMessage] = None
self._from_data(data)
def _from_data(self, data):
self.id = int(data['id'])
self.type = try_enum(InteractionType, data['type'])
self.data = data.get('data')
self.token = data['token']
self.version = data['version']
self.channel_id = utils._get_as_snowflake(data, 'channel_id')
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
self.application_id = utils._get_as_snowflake(data, 'application_id')
def _from_data(self, data: InteractionPayload):
self.id: int = int(data["id"])
self.type: InteractionType = try_enum(InteractionType, data["type"])
self.data: Optional[InteractionData] = data.get("data")
self.token: str = data["token"]
self.version: int = data["version"]
self.channel_id: Optional[int] = utils._get_as_snowflake(data, "channel_id")
self.guild_id: Optional[int] = utils._get_as_snowflake(data, "guild_id")
self.application_id: int = int(data["application_id"])
self.message: Optional[Message]
try:
self.message = Message(state=self._state, channel=self.channel, data=data["message"]) # type: ignore
except KeyError:
self.message = None
self.user: Optional[Union[User, Member]] = None
self._permissions: int = 0
# TODO: there's a potential data loss here
if self.guild_id:
guild = self.guild or Object(id=self.guild_id)
try:
member = data["member"] # type: ignore
except KeyError:
pass
else:
self.user = Member(state=self._state, guild=guild, data=member) # type: ignore
self._permissions = int(member.get("permissions", 0))
else:
try:
self.user = User(state=self._state, data=data["user"])
except KeyError:
pass
@property
def guild(self):
def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: The guild the interaction was sent from."""
return self._state and self._state.get_guild(self.guild_id)
return self._state and self._state._get_guild(self.guild_id)
@property
def channel(self):
"""Optional[:class:`abc.GuildChannel`]: The channel the interaction was sent from.
@utils.cached_slot_property("_cs_channel")
def channel(self) -> Optional[InteractionChannel]:
"""Optional[Union[:class:`abc.GuildChannel`, :class:`PartialMessageable`, :class:`Thread`]]: The channel the interaction was sent from.
Note that due to a Discord limitation, DM channels are not resolved since there is
no data to complete them.
no data to complete them. These are :class:`PartialMessageable` instead.
"""
guild = self.guild
return guild and guild.get_channel(self.channel_id)
channel = guild and guild._resolve_channel(self.channel_id)
if channel is None:
if self.channel_id is not None:
type = ChannelType.text if self.guild_id is not None else ChannelType.private
return PartialMessageable(state=self._state, id=self.channel_id, type=type)
return None
return channel # type: ignore
@property
def permissions(self) -> Permissions:
""":class:`Permissions`: The resolved permissions of the member in the channel, including overwrites.
In a non-guild context where this doesn't apply, an empty permissions object is returned.
"""
return Permissions(self._permissions)
@utils.cached_slot_property("_cs_response")
def response(self) -> InteractionResponse:
""":class:`InteractionResponse`: Returns an object responsible for handling responding to the interaction.
A response can only be done once. If secondary messages need to be sent, consider using :attr:`followup`
instead.
"""
return InteractionResponse(self)
@utils.cached_slot_property("_cs_followup")
def followup(self) -> Webhook:
""":class:`Webhook`: Returns the follow up webhook for follow up interactions."""
payload = {
"id": self.application_id,
"type": 3,
"token": self.token,
}
return Webhook.from_state(data=payload, state=self._state)
async def original_message(self) -> InteractionMessage:
"""|coro|
Fetches the original interaction response message associated with the interaction.
If the interaction response was :meth:`InteractionResponse.send_message` then this would
return the message that was sent using that response. Otherwise, this would return
the message that triggered the interaction.
Repeated calls to this will return a cached value.
Raises
-------
HTTPException
Fetching the original response message failed.
ClientException
The channel for the message could not be resolved.
Returns
--------
InteractionMessage
The original interaction response message.
"""
if self._original_message is not None:
return self._original_message
# TODO: fix later to not raise?
channel = self.channel
if channel is None:
raise ClientException("Channel for message could not be resolved")
adapter = async_context.get()
data = await adapter.get_original_interaction_response(
application_id=self.application_id,
token=self.token,
session=self._session,
)
state = _InteractionMessageState(self, self._state)
message = InteractionMessage(state=state, channel=channel, data=data) # type: ignore
self._original_message = message
return message
async def edit_original_message(
self,
*,
content: Optional[str] = MISSING,
embeds: List[Embed] = MISSING,
embed: Optional[Embed] = MISSING,
file: File = MISSING,
files: List[File] = MISSING,
view: Optional[View] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
) -> InteractionMessage:
"""|coro|
Edits the original interaction response message.
This is a lower level interface to :meth:`InteractionMessage.edit` in case
you do not want to fetch the message and save an HTTP request.
This method is also the only way to edit the original message if
the message sent was ephemeral.
Parameters
------------
content: Optional[:class:`str`]
The content to edit the message with or ``None`` to clear it.
embeds: List[:class:`Embed`]
A list of embeds to edit the message with.
embed: Optional[:class:`Embed`]
The embed to edit the message with. ``None`` suppresses the embeds.
This should not be mixed with the ``embeds`` parameter.
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.
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.
view: Optional[:class:`~discord.ui.View`]
The updated view to update this message with. If ``None`` is passed then
the view is removed.
Raises
-------
HTTPException
Editing the message failed.
Forbidden
Edited a message that is not yours.
TypeError
You specified both ``embed`` and ``embeds`` or ``file`` and ``files``
ValueError
The length of ``embeds`` was invalid.
Returns
--------
:class:`InteractionMessage`
The newly edited message.
"""
previous_mentions: Optional[AllowedMentions] = self._state.allowed_mentions
params = handle_message_parameters(
content=content,
file=file,
files=files,
embed=embed,
embeds=embeds,
view=view,
allowed_mentions=allowed_mentions,
previous_allowed_mentions=previous_mentions,
)
adapter = async_context.get()
data = await adapter.edit_original_interaction_response(
self.application_id,
self.token,
session=self._session,
payload=params.payload,
multipart=params.multipart,
files=params.files,
)
# The message channel types should always match
message = InteractionMessage(state=self._state, channel=self.channel, data=data) # type: ignore
if view and not view.is_finished():
self._state.store_view(view, message.id)
return message
async def delete_original_message(self) -> None:
"""|coro|
Deletes the original interaction response message.
This is a lower level interface to :meth:`InteractionMessage.delete` in case
you do not want to fetch the message and save an HTTP request.
Raises
-------
HTTPException
Deleting the message failed.
Forbidden
Deleted a message that is not yours.
"""
adapter = async_context.get()
await adapter.delete_original_interaction_response(
self.application_id,
self.token,
session=self._session,
)
class InteractionResponse:
"""Represents a Discord interaction response.
This type can be accessed through :attr:`Interaction.response`.
.. versionadded:: 2.0
"""
__slots__: Tuple[str, ...] = (
"responded_at",
"_parent",
)
def __init__(self, parent: Interaction):
self.responded_at: Optional[datetime] = None
self._parent: Interaction = parent
def is_done(self) -> bool:
""":class:`bool`: Indicates whether an interaction response has been done before.
An interaction can only be responded to once.
"""
return self.responded_at is not None
async def defer(self, *, ephemeral: bool = False) -> None:
"""|coro|
Defers the interaction response.
This is typically used when the interaction is acknowledged
and a secondary action will be done later.
Parameters
-----------
ephemeral: :class:`bool`
Indicates whether the deferred message will eventually be ephemeral.
This only applies for interactions of type :attr:`InteractionType.application_command`.
Raises
-------
HTTPException
Deferring the interaction failed.
InteractionResponded
This interaction has already been responded to before.
"""
if self.is_done():
raise InteractionResponded(self._parent)
defer_type: int = 0
data: Optional[Dict[str, Any]] = None
parent = self._parent
if parent.type is InteractionType.component:
defer_type = InteractionResponseType.deferred_message_update.value
elif parent.type is InteractionType.application_command:
defer_type = InteractionResponseType.deferred_channel_message.value
if ephemeral:
data = {"flags": 64}
if defer_type:
adapter = async_context.get()
await adapter.create_interaction_response(
parent.id, parent.token, session=parent._session, type=defer_type, data=data
)
self.responded_at = utils.utcnow()
async def pong(self) -> None:
"""|coro|
Pongs the ping interaction.
This should rarely be used.
Raises
-------
HTTPException
Ponging the interaction failed.
InteractionResponded
This interaction has already been responded to before.
"""
if self.is_done():
raise InteractionResponded(self._parent)
parent = self._parent
if parent.type is InteractionType.ping:
adapter = async_context.get()
await adapter.create_interaction_response(
parent.id, parent.token, session=parent._session, type=InteractionResponseType.pong.value
)
self.responded_at = utils.utcnow()
async def send_message(
self,
content: Optional[Any] = None,
*,
embed: Embed = MISSING,
embeds: List[Embed] = MISSING,
view: View = MISSING,
tts: bool = False,
ephemeral: bool = False,
) -> None:
"""|coro|
Responds to this interaction by sending a message.
Parameters
-----------
content: Optional[:class:`str`]
The content of the message to send.
embeds: List[:class:`Embed`]
A list of embeds to send with the content. Maximum of 10. This cannot
be mixed with the ``embed`` parameter.
embed: :class:`Embed`
The rich embed for the content to send. This cannot be mixed with
``embeds`` parameter.
tts: :class:`bool`
Indicates if the message should be sent using text-to-speech.
view: :class:`discord.ui.View`
The view to send with the message.
ephemeral: :class:`bool`
Indicates if the message should only be visible to the user who started the interaction.
If a view is sent with an ephemeral message and it has no timeout set then the timeout
is set to 15 minutes.
Raises
-------
HTTPException
Sending the message failed.
TypeError
You specified both ``embed`` and ``embeds``.
ValueError
The length of ``embeds`` was invalid.
InteractionResponded
This interaction has already been responded to before.
"""
if self.is_done():
raise InteractionResponded(self._parent)
payload: Dict[str, Any] = {
"tts": tts,
}
if embed is not MISSING and embeds is not MISSING:
raise TypeError("cannot mix embed and embeds keyword arguments")
if embed is not MISSING:
embeds = [embed]
if embeds:
if len(embeds) > 10:
raise ValueError("embeds cannot exceed maximum of 10 elements")
payload["embeds"] = [e.to_dict() for e in embeds]
if content is not None:
payload["content"] = str(content)
if ephemeral:
payload["flags"] = 64
if view is not MISSING:
payload["components"] = view.to_components()
parent = self._parent
adapter = async_context.get()
await adapter.create_interaction_response(
parent.id,
parent.token,
session=parent._session,
type=InteractionResponseType.channel_message.value,
data=payload,
)
if view is not MISSING:
if ephemeral and view.timeout is None:
view.timeout = 15 * 60.0
self._parent._state.store_view(view)
self.responded_at = utils.utcnow()
async def edit_message(
self,
*,
content: Optional[Any] = MISSING,
embed: Optional[Embed] = MISSING,
embeds: List[Embed] = MISSING,
attachments: List[Attachment] = MISSING,
view: Optional[View] = MISSING,
) -> None:
"""|coro|
Responds to this interaction by editing the original message of
a component interaction.
Parameters
-----------
content: Optional[:class:`str`]
The new content to replace the message with. ``None`` removes the content.
embeds: List[:class:`Embed`]
A list of embeds to edit the message with.
embed: Optional[:class:`Embed`]
The embed to edit the message with. ``None`` suppresses the embeds.
This should not be mixed with the ``embeds`` parameter.
attachments: List[:class:`Attachment`]
A list of attachments to keep in the message. If ``[]`` is passed
then all attachments are removed.
view: Optional[:class:`~discord.ui.View`]
The updated view to update this message with. If ``None`` is passed then
the view is removed.
Raises
-------
HTTPException
Editing the message failed.
TypeError
You specified both ``embed`` and ``embeds``.
InteractionResponded
This interaction has already been responded to before.
"""
if self.is_done():
raise InteractionResponded(self._parent)
parent = self._parent
msg = parent.message
state = parent._state
message_id = msg.id if msg else None
if parent.type is not InteractionType.component:
return
payload = {}
if content is not MISSING:
if content is None:
payload["content"] = None
else:
payload["content"] = str(content)
if embed is not MISSING and embeds is not MISSING:
raise TypeError("cannot mix both embed and embeds keyword arguments")
if embed is not MISSING:
if embed is None:
embeds = []
else:
embeds = [embed]
if embeds is not MISSING:
payload["embeds"] = [e.to_dict() for e in embeds]
if attachments is not MISSING:
payload["attachments"] = [a.to_dict() for a in attachments]
if view is not MISSING:
state.prevent_view_updates_for(message_id)
if view is None:
payload["components"] = []
else:
payload["components"] = view.to_components()
adapter = async_context.get()
await adapter.create_interaction_response(
parent.id,
parent.token,
session=parent._session,
type=InteractionResponseType.message_update.value,
data=payload,
)
if view and not view.is_finished():
state.store_view(view, message_id)
self.responded_at = utils.utcnow()
async def autocomplete_result(self, choices: List[ApplicationCommandOptionChoice]):
"""|coro|
Responds to this autocomplete interaction with the resulting choices.
This should rarely be used.
Parameters
-----------
choices: List[Dict[:class:`str`, :class:`str`]]
The choices to be shown in the autocomplete UI of the user.
Must be a list of dictionaries with the ``name`` and ``value`` keys.
Raises
-------
HTTPException
Responding to the interaction failed.
InteractionResponded
This interaction has already been responded to before.
"""
if self.is_done():
raise InteractionResponded(self._parent)
parent = self._parent
if parent.type is not InteractionType.application_command_autocomplete:
return
adapter = async_context.get()
await adapter.create_interaction_response(
parent.id,
parent.token,
session=parent._session,
type=InteractionResponseType.application_command_autocomplete_result.value,
data={"choices": choices},
)
self.responded_at = utils.utcnow()
class _InteractionMessageState:
__slots__ = ("_parent", "_interaction")
def __init__(self, interaction: Interaction, parent: ConnectionState):
self._interaction: Interaction = interaction
self._parent: ConnectionState = parent
def _get_guild(self, guild_id):
return self._parent._get_guild(guild_id)
def store_user(self, data):
return self._parent.store_user(data)
def create_user(self, data):
return self._parent.create_user(data)
@property
def http(self):
return self._parent.http
def __getattr__(self, attr):
return getattr(self._parent, attr)
class InteractionMessage(Message):
"""Represents the original interaction response message.
This allows you to edit or delete the message associated with
the interaction response. To retrieve this object see :meth:`Interaction.original_message`.
This inherits from :class:`discord.Message` with changes to
:meth:`edit` and :meth:`delete` to work.
.. versionadded:: 2.0
"""
__slots__ = ()
_state: _InteractionMessageState
async def edit(
self,
content: Optional[str] = MISSING,
embeds: List[Embed] = MISSING,
embed: Optional[Embed] = MISSING,
file: File = MISSING,
files: List[File] = MISSING,
view: Optional[View] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
) -> InteractionMessage:
"""|coro|
Edits the message.
Parameters
------------
content: Optional[:class:`str`]
The content to edit the message with or ``None`` to clear it.
embeds: List[:class:`Embed`]
A list of embeds to edit the message with.
embed: Optional[:class:`Embed`]
The embed to edit the message with. ``None`` suppresses the embeds.
This should not be mixed with the ``embeds`` parameter.
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.
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.
view: Optional[:class:`~discord.ui.View`]
The updated view to update this message with. If ``None`` is passed then
the view is removed.
Raises
-------
HTTPException
Editing the message failed.
Forbidden
Edited a message that is not yours.
TypeError
You specified both ``embed`` and ``embeds`` or ``file`` and ``files``
ValueError
The length of ``embeds`` was invalid.
Returns
---------
:class:`InteractionMessage`
The newly edited message.
"""
return await self._state._interaction.edit_original_message(
content=content,
embeds=embeds,
embed=embed,
file=file,
files=files,
view=view,
allowed_mentions=allowed_mentions,
)
async def delete(self, *, delay: Optional[float] = None) -> None:
"""|coro|
Deletes the message.
Parameters
-----------
delay: Optional[:class:`float`]
If provided, the number of seconds to wait before deleting the message.
The waiting is done in the background and deletion failures are ignored.
Raises
------
Forbidden
You do not have proper permissions to delete the message.
NotFound
The message was deleted already.
HTTPException
Deleting the message failed.
"""
if delay is not None:
async def inner_call(delay: float = delay):
await asyncio.sleep(delay)
try:
await self._state._interaction.delete_original_message()
except HTTPException:
pass
asyncio.create_task(inner_call())
else:
await self._state._interaction.delete_original_message()

View File

@ -22,18 +22,42 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import List, Optional, Type, TypeVar, Union, TYPE_CHECKING
from .asset import Asset
from .utils import parse_time, snowflake_time, _get_as_snowflake
from .object import Object
from .mixins import Hashable
from .enums import ChannelType, VerificationLevel, try_enum
from .enums import ChannelType, VerificationLevel, InviteTarget, try_enum
from .appinfo import PartialAppInfo
__all__ = (
'PartialInviteChannel',
'PartialInviteGuild',
'Invite',
"PartialInviteChannel",
"PartialInviteGuild",
"Invite",
)
if TYPE_CHECKING:
from .types.invite import (
Invite as InvitePayload,
InviteGuild as InviteGuildPayload,
GatewayInvite as GatewayInvitePayload,
)
from .types.channel import (
PartialChannel as InviteChannelPayload,
)
from .state import ConnectionState
from .guild import Guild
from .abc import GuildChannel
from .user import User
InviteGuildType = Union[Guild, "PartialInviteGuild", Object]
InviteChannelType = Union[GuildChannel, "PartialInviteChannel", Object]
import datetime
class PartialInviteChannel:
"""Represents a "partial" invite channel.
@ -68,29 +92,30 @@ class PartialInviteChannel:
The partial channel's type.
"""
__slots__ = ('id', 'name', 'type')
__slots__ = ("id", "name", "type")
def __init__(self, **kwargs):
self.id = kwargs.pop('id')
self.name = kwargs.pop('name')
self.type = kwargs.pop('type')
def __init__(self, data: InviteChannelPayload):
self.id: int = int(data["id"])
self.name: str = data["name"]
self.type: ChannelType = try_enum(ChannelType, data["type"])
def __str__(self):
def __str__(self) -> str:
return self.name
def __repr__(self):
return '<PartialInviteChannel id={0.id} name={0.name} type={0.type!r}>'.format(self)
def __repr__(self) -> str:
return f"<PartialInviteChannel id={self.id} name={self.name} type={self.type!r}>"
@property
def mention(self):
def mention(self) -> str:
""":class:`str`: The string that allows you to mention the channel."""
return f'<#{self.id}>'
return f"<#{self.id}>"
@property
def created_at(self):
def created_at(self) -> datetime.datetime:
""":class:`datetime.datetime`: Returns the channel's creation time in UTC."""
return snowflake_time(self.id)
class PartialInviteGuild:
"""Represents a "partial" invite guild.
@ -125,93 +150,61 @@ class PartialInviteGuild:
The partial guild's verification level.
features: List[:class:`str`]
A list of features the guild has. See :attr:`Guild.features` for more information.
icon: Optional[:class:`str`]
The partial guild's icon.
banner: Optional[:class:`str`]
The partial guild's banner.
splash: Optional[:class:`str`]
The partial guild's invite splash.
description: Optional[:class:`str`]
The partial guild's description.
"""
__slots__ = ('_state', 'features', 'icon', 'banner', 'id', 'name', 'splash',
'verification_level', 'description')
__slots__ = ("_state", "features", "_icon", "_banner", "id", "name", "_splash", "verification_level", "description")
def __init__(self, state, data, id):
self._state = state
self.id = id
self.name = data['name']
self.features = data.get('features', [])
self.icon = data.get('icon')
self.banner = data.get('banner')
self.splash = data.get('splash')
self.verification_level = try_enum(VerificationLevel, data.get('verification_level'))
self.description = data.get('description')
def __init__(self, state: ConnectionState, data: InviteGuildPayload, id: int):
self._state: ConnectionState = state
self.id: int = id
self.name: str = data["name"]
self.features: List[str] = data.get("features", [])
self._icon: Optional[str] = data.get("icon")
self._banner: Optional[str] = data.get("banner")
self._splash: Optional[str] = data.get("splash")
self.verification_level: VerificationLevel = try_enum(VerificationLevel, data.get("verification_level"))
self.description: Optional[str] = data.get("description")
def __str__(self):
def __str__(self) -> str:
return self.name
def __repr__(self):
return '<{0.__class__.__name__} id={0.id} name={0.name!r} features={0.features} ' \
'description={0.description!r}>'.format(self)
def __repr__(self) -> str:
return (
f"<{self.__class__.__name__} id={self.id} name={self.name!r} features={self.features} "
f"description={self.description!r}>"
)
@property
def created_at(self):
def created_at(self) -> datetime.datetime:
""":class:`datetime.datetime`: Returns the guild's creation time in UTC."""
return snowflake_time(self.id)
@property
def icon_url(self):
""":class:`Asset`: Returns the guild's icon asset."""
return self.icon_url_as()
def is_icon_animated(self):
""":class:`bool`: Returns ``True`` if the guild has an animated icon.
.. versionadded:: 1.4
"""
return bool(self.icon and self.icon.startswith('a_'))
def icon_url_as(self, *, format=None, static_format='webp', size=1024):
"""The same operation as :meth:`Guild.icon_url_as`.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_guild_icon(self._state, self, format=format, static_format=static_format, size=size)
def icon(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns the guild's icon asset, if available."""
if self._icon is None:
return None
return Asset._from_guild_icon(self._state, self.id, self._icon)
@property
def banner_url(self):
""":class:`Asset`: Returns the guild's banner asset."""
return self.banner_url_as()
def banner_url_as(self, *, format='webp', size=2048):
"""The same operation as :meth:`Guild.banner_url_as`.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_guild_image(self._state, self.id, self.banner, 'banners', format=format, size=size)
def banner(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns the guild's banner asset, if available."""
if self._banner is None:
return None
return Asset._from_guild_image(self._state, self.id, self._banner, path="banners")
@property
def splash_url(self):
""":class:`Asset`: Returns the guild's invite splash asset."""
return self.splash_url_as()
def splash(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns the guild's invite splash asset, if available."""
if self._splash is None:
return None
return Asset._from_guild_image(self._state, self.id, self._splash, path="splashes")
def splash_url_as(self, *, format='webp', size=2048):
"""The same operation as :meth:`Guild.splash_url_as`.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_guild_image(self._state, self.id, self.splash, 'splashes', format=format, size=size)
I = TypeVar("I", bound="Invite")
class Invite(Hashable):
r"""Represents a Discord :class:`Guild` or :class:`abc.GuildChannel` invite.
@ -237,32 +230,35 @@ class Invite(Hashable):
Returns the invite URL.
The following table illustrates what methods will obtain the attributes:
+------------------------------------+----------------------------------------------------------+
| Attribute | Method |
+====================================+==========================================================+
| :attr:`max_age` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
+------------------------------------+----------------------------------------------------------+
| :attr:`max_uses` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
+------------------------------------+----------------------------------------------------------+
| :attr:`created_at` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
+------------------------------------+----------------------------------------------------------+
| :attr:`temporary` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
+------------------------------------+----------------------------------------------------------+
| :attr:`uses` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
+------------------------------------+----------------------------------------------------------+
| :attr:`approximate_member_count` | :meth:`Client.fetch_invite` |
+------------------------------------+----------------------------------------------------------+
| :attr:`approximate_presence_count` | :meth:`Client.fetch_invite` |
+------------------------------------+----------------------------------------------------------+
+------------------------------------+------------------------------------------------------------+
| Attribute | Method |
+====================================+============================================================+
| :attr:`max_age` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
+------------------------------------+------------------------------------------------------------+
| :attr:`max_uses` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
+------------------------------------+------------------------------------------------------------+
| :attr:`created_at` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
+------------------------------------+------------------------------------------------------------+
| :attr:`temporary` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
+------------------------------------+------------------------------------------------------------+
| :attr:`uses` | :meth:`abc.GuildChannel.invites`\, :meth:`Guild.invites` |
+------------------------------------+------------------------------------------------------------+
| :attr:`approximate_member_count` | :meth:`Client.fetch_invite` with `with_counts` enabled |
+------------------------------------+------------------------------------------------------------+
| :attr:`approximate_presence_count` | :meth:`Client.fetch_invite` with `with_counts` enabled |
+------------------------------------+------------------------------------------------------------+
| :attr:`expires_at` | :meth:`Client.fetch_invite` with `with_expiration` enabled |
+------------------------------------+------------------------------------------------------------+
If it's not in the table above then it is available by all methods.
Attributes
-----------
max_age: :class:`int`
How long the before the invite expires in seconds.
How long 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.
@ -280,105 +276,190 @@ class Invite(Hashable):
max_uses: :class:`int`
How many times the invite can be used.
A value of ``0`` indicates that it has unlimited uses.
inviter: :class:`User`
inviter: Optional[:class:`User`]
The user who created the invite.
approximate_member_count: Optional[:class:`int`]
The approximate number of members in the guild.
approximate_presence_count: Optional[:class:`int`]
The approximate number of members currently active in the guild.
This includes idle, dnd, online, and invisible members. Offline members are excluded.
expires_at: Optional[:class:`datetime.datetime`]
The expiration date of the invite. If the value is ``None`` when received through
`Client.fetch_invite` with `with_expiration` enabled, the invite will never expire.
.. versionadded:: 2.0
channel: Union[:class:`abc.GuildChannel`, :class:`Object`, :class:`PartialInviteChannel`]
The channel the invite is for.
target_type: :class:`InviteTarget`
The type of target for the voice channel invite.
.. versionadded:: 2.0
target_user: Optional[:class:`User`]
The user whose stream to display for this invite, if any.
.. versionadded:: 2.0
target_application: Optional[:class:`PartialAppInfo`]
The embedded application the invite targets, if any.
.. versionadded:: 2.0
"""
__slots__ = ('max_age', 'code', 'guild', 'revoked', 'created_at', 'uses',
'temporary', 'max_uses', 'inviter', 'channel', '_state',
'approximate_member_count', 'approximate_presence_count' )
__slots__ = (
"max_age",
"code",
"guild",
"revoked",
"created_at",
"uses",
"temporary",
"max_uses",
"inviter",
"channel",
"target_user",
"target_type",
"_state",
"approximate_member_count",
"approximate_presence_count",
"target_application",
"expires_at",
)
BASE = 'https://discord.gg'
BASE = "https://discord.gg"
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')
self.approximate_presence_count = data.get('approximate_presence_count')
self.approximate_member_count = data.get('approximate_member_count')
def __init__(
self,
*,
state: ConnectionState,
data: InvitePayload,
guild: Optional[Union[PartialInviteGuild, Guild]] = None,
channel: Optional[Union[PartialInviteChannel, GuildChannel]] = None,
):
self._state: ConnectionState = state
self.max_age: Optional[int] = data.get("max_age")
self.code: str = data["code"]
self.guild: Optional[InviteGuildType] = self._resolve_guild(data.get("guild"), guild)
self.revoked: Optional[bool] = data.get("revoked")
self.created_at: Optional[datetime.datetime] = parse_time(data.get("created_at"))
self.temporary: Optional[bool] = data.get("temporary")
self.uses: Optional[int] = data.get("uses")
self.max_uses: Optional[int] = data.get("max_uses")
self.approximate_presence_count: Optional[int] = data.get("approximate_presence_count")
self.approximate_member_count: Optional[int] = data.get("approximate_member_count")
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')
expires_at = data.get("expires_at", None)
self.expires_at: Optional[datetime.datetime] = parse_time(expires_at) if expires_at else None
inviter_data = data.get("inviter")
self.inviter: Optional[User] = None if inviter_data is None else self._state.create_user(inviter_data)
self.channel: Optional[InviteChannelType] = self._resolve_channel(data.get("channel"), channel)
target_user_data = data.get("target_user")
self.target_user: Optional[User] = (
None if target_user_data is None else self._state.create_user(target_user_data)
)
self.target_type: InviteTarget = try_enum(InviteTarget, data.get("target_type", 0))
application = data.get("target_application")
self.target_application: Optional[PartialAppInfo] = (
PartialAppInfo(data=application, state=state) if application else None
)
@classmethod
def from_incomplete(cls, *, state, data):
def from_incomplete(cls: Type[I], *, state: ConnectionState, data: InvitePayload) -> I:
guild: Optional[Union[Guild, PartialInviteGuild]]
try:
guild_id = int(data['guild']['id'])
guild_data = data["guild"]
except KeyError:
# If we're here, then this is a group DM
guild = None
else:
guild_id = int(guild_data["id"])
guild = state._get_guild(guild_id)
if guild is None:
# If it's not cached, then it has to be a partial guild
guild_data = data['guild']
guild = PartialInviteGuild(state, guild_data, guild_id)
# As far as I know, invites always need a channel
# So this should never raise.
channel_data = data['channel']
channel_id = int(channel_data['id'])
channel_type = try_enum(ChannelType, channel_data['type'])
channel = PartialInviteChannel(id=channel_id, name=channel_data['name'], type=channel_type)
channel: Union[PartialInviteChannel, GuildChannel] = PartialInviteChannel(data["channel"])
if guild is not None and not isinstance(guild, PartialInviteGuild):
# Upgrade the partial data if applicable
channel = guild.get_channel(channel_id) or channel
channel = guild.get_channel(channel.id) or channel
data['guild'] = guild
data['channel'] = channel
return cls(state=state, data=data)
return cls(state=state, data=data, guild=guild, channel=channel)
@classmethod
def from_gateway(cls, *, state, data):
guild_id = _get_as_snowflake(data, 'guild_id')
guild = state._get_guild(guild_id)
channel_id = _get_as_snowflake(data, 'channel_id')
def from_gateway(cls: Type[I], *, state: ConnectionState, data: GatewayInvitePayload) -> I:
guild_id: Optional[int] = _get_as_snowflake(data, "guild_id")
guild: Optional[Union[Guild, Object]] = state._get_guild(guild_id)
channel_id = int(data["channel_id"])
if guild is not None:
channel = guild.get_channel(channel_id) or Object(id=channel_id)
channel = guild.get_channel(channel_id) or Object(id=channel_id) # type: ignore
else:
guild = Object(id=guild_id)
guild = Object(id=guild_id) if guild_id is not None else None
channel = Object(id=channel_id)
data['guild'] = guild
data['channel'] = channel
return cls(state=state, data=data)
return cls(state=state, data=data, guild=guild, channel=channel) # type: ignore
def __str__(self):
def _resolve_guild(
self,
data: Optional[InviteGuildPayload],
guild: Optional[Union[Guild, PartialInviteGuild]] = None,
) -> Optional[InviteGuildType]:
if guild is not None:
return guild
if data is None:
return None
guild_id = int(data["id"])
return PartialInviteGuild(self._state, data, guild_id)
def _resolve_channel(
self,
data: Optional[InviteChannelPayload],
channel: Optional[Union[PartialInviteChannel, GuildChannel]] = None,
) -> Optional[InviteChannelType]:
if channel is not None:
return channel
if data is None:
return None
return PartialInviteChannel(data)
def __str__(self) -> str:
return self.url
def __repr__(self):
return '<Invite code={0.code!r} guild={0.guild!r} ' \
'online={0.approximate_presence_count} ' \
'members={0.approximate_member_count}>'.format(self)
def __int__(self) -> int:
return 0 # To keep the object compatible with the hashable abc.
def __hash__(self):
def __repr__(self) -> str:
return (
f"<Invite code={self.code!r} guild={self.guild!r} "
f"online={self.approximate_presence_count} "
f"members={self.approximate_member_count}>"
)
def __hash__(self) -> int:
return hash(self.code)
@property
def id(self):
def id(self) -> str:
""":class:`str`: Returns the proper code portion of the invite."""
return self.code
@property
def url(self):
def url(self) -> str:
""":class:`str`: A property that retrieves the invite URL."""
return self.BASE + '/' + self.code
return self.BASE + "/" + self.code
async def delete(self, *, reason=None):
async def delete(self, *, reason: Optional[str] = None):
"""|coro|
Revokes the instant invite.

View File

@ -26,42 +26,64 @@ from __future__ import annotations
import asyncio
import datetime
from typing import TYPE_CHECKING, TypeVar, Optional, Any, Callable, Union, List, AsyncIterator, Coroutine
from typing import Awaitable, TYPE_CHECKING, TypeVar, Optional, Any, Callable, Union, List, AsyncIterator
from .errors import NoMoreItems
from .utils import time_snowflake, maybe_coroutine
from .utils import snowflake_time, time_snowflake, maybe_coroutine
from .object import Object
from .audit_logs import AuditLogEntry
__all__ = (
'ReactionIterator',
'HistoryIterator',
'AuditLogIterator',
'GuildIterator',
'MemberIterator',
"ReactionIterator",
"HistoryIterator",
"AuditLogIterator",
"GuildIterator",
"MemberIterator",
)
if TYPE_CHECKING:
from .types.audit_log import (
AuditLog as AuditLogPayload,
)
from .types.guild import (
Guild as GuildPayload,
)
from .types.message import (
Message as MessagePayload,
)
from .types.user import (
PartialUser as PartialUserPayload,
)
from .types.threads import (
Thread as ThreadPayload,
)
from .member import Member
from .user import User
from .message import Message
from .audit_logs import AuditLogEntry
from .guild import Guild
from .threads import Thread
from .abc import Snowflake
T = TypeVar('T')
OT = TypeVar('OT')
_Func = Callable[[T], Union[OT, Coroutine[Any, Any, OT]]]
_Predicate = Callable[[T], Union[T, Coroutine[Any, Any, T]]]
T = TypeVar("T")
OT = TypeVar("OT")
_Func = Callable[[T], Union[OT, Awaitable[OT]]]
OLDEST_OBJECT = Object(id=0)
class _AsyncIterator(AsyncIterator[T]):
__slots__ = ()
def get(self, **attrs: Any) -> Optional[T]:
def predicate(elem):
async def next(self) -> T:
raise NotImplementedError
def get(self, **attrs: Any) -> Awaitable[Optional[T]]:
def predicate(elem: T):
for attr, val in attrs.items():
nested = attr.split('__')
nested = attr.split("__")
obj = elem
for attribute in nested:
obj = getattr(obj, attribute)
@ -72,7 +94,7 @@ class _AsyncIterator(AsyncIterator[T]):
return self.find(predicate)
async def find(self, predicate: _Predicate[T]) -> Optional[T]:
async def find(self, predicate: _Func[T, bool]) -> Optional[T]:
while True:
try:
elem = await self.next()
@ -85,13 +107,13 @@ class _AsyncIterator(AsyncIterator[T]):
def chunk(self, max_size: int) -> _ChunkedAsyncIterator[T]:
if max_size <= 0:
raise ValueError('async iterator chunk sizes must be greater than 0.')
raise ValueError("async iterator chunk sizes must be greater than 0.")
return _ChunkedAsyncIterator(self, max_size)
def map(self, func: _Func[T, OT]) -> _MappedAsyncIterator[OT]:
return _MappedAsyncIterator(self, func)
def filter(self, predicate: _Predicate[T]) -> _FilteredAsyncIterator[T]:
def filter(self, predicate: _Func[T, bool]) -> _FilteredAsyncIterator[T]:
return _FilteredAsyncIterator(self, predicate)
async def flatten(self) -> List[T]:
@ -103,16 +125,18 @@ class _AsyncIterator(AsyncIterator[T]):
except NoMoreItems:
raise StopAsyncIteration()
def _identity(x):
return x
class _ChunkedAsyncIterator(_AsyncIterator[T]):
class _ChunkedAsyncIterator(_AsyncIterator[List[T]]):
def __init__(self, iterator, max_size):
self.iterator = iterator
self.max_size = max_size
async def next(self) -> T:
ret = []
async def next(self) -> List[T]:
ret: List[T] = []
n = 0
while n < self.max_size:
try:
@ -126,6 +150,7 @@ class _ChunkedAsyncIterator(_AsyncIterator[T]):
n += 1
return ret
class _MappedAsyncIterator(_AsyncIterator[T]):
def __init__(self, iterator, func):
self.iterator = iterator
@ -136,6 +161,7 @@ class _MappedAsyncIterator(_AsyncIterator[T]):
item = await self.iterator.next()
return await maybe_coroutine(self.func, item)
class _FilteredAsyncIterator(_AsyncIterator[T]):
def __init__(self, iterator, predicate):
self.iterator = iterator
@ -155,7 +181,8 @@ class _FilteredAsyncIterator(_AsyncIterator[T]):
if ret:
return item
class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
class ReactionIterator(_AsyncIterator[Union["User", "Member"]]):
def __init__(self, message, emoji, limit=100, after=None):
self.message = message
self.limit = limit
@ -168,7 +195,7 @@ class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
self.channel_id = message.channel.id
self.users = asyncio.Queue()
async def next(self) -> T:
async def next(self) -> Union[User, Member]:
if self.users.empty():
await self.fill_users()
@ -185,25 +212,28 @@ class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
retrieve = self.limit if self.limit <= 100 else 100
after = self.after.id if self.after else None
data = await self.getter(self.channel_id, self.message.id, self.emoji, retrieve, after=after)
data: List[PartialUserPayload] = await self.getter(
self.channel_id, self.message.id, self.emoji, retrieve, after=after
)
if data:
self.limit -= retrieve
self.after = Object(id=int(data[-1]['id']))
self.after = Object(id=int(data[-1]["id"]))
if self.guild is None or isinstance(self.guild, Object):
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_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['Message']):
class HistoryIterator(_AsyncIterator["Message"]):
"""Iterator for receiving a channel's message history.
The messages endpoint has two behaviours we care about here:
@ -237,8 +267,7 @@ class HistoryIterator(_AsyncIterator['Message']):
``True`` if `after` is specified, otherwise ``False``.
"""
def __init__(self, messageable, limit,
before=None, after=None, around=None, oldest_first=None):
def __init__(self, messageable, limit, before=None, after=None, around=None, oldest_first=None):
if isinstance(before, datetime.datetime):
before = Object(id=time_snowflake(before, high=False))
@ -266,30 +295,30 @@ class HistoryIterator(_AsyncIterator['Message']):
if self.around:
if self.limit is None:
raise ValueError('history does not support around with limit=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
self._retrieve_messages = self._retrieve_messages_around_strategy
self._retrieve_messages = self._retrieve_messages_around_strategy # type: ignore
if self.before and self.after:
self._filter = lambda m: self.after.id < int(m['id']) < self.before.id
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
self._filter = lambda m: int(m["id"]) < self.before.id
elif self.after:
self._filter = lambda m: self.after.id < int(m['id'])
self._filter = lambda m: self.after.id < int(m["id"])
else:
if self.reverse:
self._retrieve_messages = self._retrieve_messages_after_strategy
if (self.before):
self._filter = lambda m: int(m['id']) < self.before.id
self._retrieve_messages = self._retrieve_messages_after_strategy # type: ignore
if self.before:
self._filter = lambda m: int(m["id"]) < self.before.id
else:
self._retrieve_messages = self._retrieve_messages_before_strategy
if (self.after and self.after != OLDEST_OBJECT):
self._filter = lambda m: int(m['id']) > self.after.id
self._retrieve_messages = self._retrieve_messages_before_strategy # type: ignore
if self.after and self.after != OLDEST_OBJECT:
self._filter = lambda m: int(m["id"]) > self.after.id
async def next(self) -> T:
async def next(self) -> Message:
if self.messages.empty():
await self.fill_messages()
@ -308,7 +337,7 @@ class HistoryIterator(_AsyncIterator['Message']):
return r > 0
async def fill_messages(self):
if not hasattr(self, 'channel'):
if not hasattr(self, "channel"):
# do the required set up
channel = await self.messageable._get_channel()
self.channel = channel
@ -316,7 +345,7 @@ class HistoryIterator(_AsyncIterator['Message']):
if self._get_retrieve():
data = await self._retrieve_messages(self.retrieve)
if len(data) < 100:
self.limit = 0 # terminate the infinite loop
self.limit = 0 # terminate the infinite loop
if self.reverse:
data = reversed(data)
@ -327,47 +356,47 @@ class HistoryIterator(_AsyncIterator['Message']):
for element in data:
await self.messages.put(self.state.create_message(channel=channel, data=element))
async def _retrieve_messages(self, retrieve):
async def _retrieve_messages(self, retrieve) -> List[Message]:
"""Retrieve messages and update next parameters."""
pass
raise NotImplementedError
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)
data: List[MessagePayload] = 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']))
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)
data: List[MessagePayload] = 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']))
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)
data: List[MessagePayload] = await self.logs_from(self.channel.id, retrieve, around=around)
self.around = None
return data
return []
class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
class AuditLogIterator(_AsyncIterator["AuditLogEntry"]):
def __init__(self, guild, limit=None, before=None, after=None, oldest_first=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))
if oldest_first is None:
self.reverse = after is not None
else:
@ -384,45 +413,45 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
self._users = {}
self._state = guild._state
self._filter = None # entry dict -> bool
self.entries = asyncio.Queue()
if self.reverse:
self._strategy = self._after_strategy
if self.before:
self._filter = lambda m: int(m['id']) < self.before.id
self._filter = lambda m: int(m["id"]) < self.before.id
else:
self._strategy = self._before_strategy
if self.after and self.after != OLDEST_OBJECT:
self._filter = lambda m: int(m['id']) > self.after.id
self._filter = lambda m: int(m["id"]) > self.after.id
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)
data: AuditLogPayload = 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', [])
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
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', [])
data: AuditLogPayload = 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
self.after = Object(id=int(entries[0]["id"]))
return data.get("users", []), entries
async def next(self) -> T:
async def next(self) -> AuditLogEntry:
if self.entries.empty():
await self._fill()
@ -446,7 +475,7 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
if self._get_retrieve():
users, data = await self._strategy(self.retrieve)
if len(data) < 100:
self.limit = 0 # terminate the infinite loop
self.limit = 0 # terminate the infinite loop
if self.reverse:
data = reversed(data)
@ -459,13 +488,13 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
for element in data:
# TODO: remove this if statement later
if element['action_type'] is None:
if element["action_type"] is None:
continue
await self.entries.put(AuditLogEntry(data=element, users=self._users, guild=self.guild))
class GuildIterator(_AsyncIterator['Guild']):
class GuildIterator(_AsyncIterator["Guild"]):
"""Iterator for receiving the client's guilds.
The guilds endpoint has the same two behaviours as described
@ -493,6 +522,7 @@ class GuildIterator(_AsyncIterator['Guild']):
after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
Object after which all guilds must be.
"""
def __init__(self, bot, limit, before=None, after=None):
if isinstance(before, datetime.datetime):
@ -512,14 +542,14 @@ class GuildIterator(_AsyncIterator['Guild']):
self.guilds = asyncio.Queue()
if self.before and self.after:
self._retrieve_guilds = self._retrieve_guilds_before_strategy
self._filter = lambda m: int(m['id']) > self.after.id
self._retrieve_guilds = self._retrieve_guilds_before_strategy # type: ignore
self._filter = lambda m: int(m["id"]) > self.after.id
elif self.after:
self._retrieve_guilds = self._retrieve_guilds_after_strategy
self._retrieve_guilds = self._retrieve_guilds_after_strategy # type: ignore
else:
self._retrieve_guilds = self._retrieve_guilds_before_strategy
self._retrieve_guilds = self._retrieve_guilds_before_strategy # type: ignore
async def next(self) -> T:
async def next(self) -> Guild:
if self.guilds.empty():
await self.fill_guilds()
@ -539,6 +569,7 @@ class GuildIterator(_AsyncIterator['Guild']):
def create_guild(self, data):
from .guild import Guild
return Guild(state=self.state, data=data)
async def fill_guilds(self):
@ -553,31 +584,32 @@ class GuildIterator(_AsyncIterator['Guild']):
for element in data:
await self.guilds.put(self.create_guild(element))
async def _retrieve_guilds(self, retrieve):
async def _retrieve_guilds(self, retrieve) -> List[Guild]:
"""Retrieve guilds and update next parameters."""
pass
raise NotImplementedError
async def _retrieve_guilds_before_strategy(self, retrieve):
"""Retrieve guilds using before parameter."""
before = self.before.id if self.before else None
data = await self.get_guilds(retrieve, before=before)
data: List[GuildPayload] = await self.get_guilds(retrieve, before=before)
if len(data):
if self.limit is not None:
self.limit -= retrieve
self.before = Object(id=int(data[-1]['id']))
self.before = Object(id=int(data[-1]["id"]))
return data
async def _retrieve_guilds_after_strategy(self, retrieve):
"""Retrieve guilds using after parameter."""
after = self.after.id if self.after else None
data = await self.get_guilds(retrieve, after=after)
data: List[GuildPayload] = await self.get_guilds(retrieve, after=after)
if len(data):
if self.limit is not None:
self.limit -= retrieve
self.after = Object(id=int(data[0]['id']))
self.after = Object(id=int(data[0]["id"]))
return data
class MemberIterator(_AsyncIterator['Member']):
class MemberIterator(_AsyncIterator["Member"]):
def __init__(self, guild, limit=1000, after=None):
if isinstance(after, datetime.datetime):
@ -591,7 +623,7 @@ class MemberIterator(_AsyncIterator['Member']):
self.get_members = self.state.http.get_members
self.members = asyncio.Queue()
async def next(self) -> T:
async def next(self) -> Member:
if self.members.empty():
await self.fill_members()
@ -618,13 +650,105 @@ class MemberIterator(_AsyncIterator['Member']):
return
if len(data) < 1000:
self.limit = 0 # terminate loop
self.limit = 0 # terminate loop
self.after = Object(id=int(data[-1]['user']['id']))
self.after = Object(id=int(data[-1]["user"]["id"]))
for element in reversed(data):
await self.members.put(self.create_member(element))
def create_member(self, data):
from .member import Member
return Member(data=data, guild=self.guild, state=self.state)
class ArchivedThreadIterator(_AsyncIterator["Thread"]):
def __init__(
self,
channel_id: int,
guild: Guild,
limit: Optional[int],
joined: bool,
private: bool,
before: Optional[Union[Snowflake, datetime.datetime]] = None,
):
self.channel_id = channel_id
self.guild = guild
self.limit = limit
self.joined = joined
self.private = private
self.http = guild._state.http
if joined and not private:
raise ValueError("Cannot iterate over joined public archived threads")
self.before: Optional[str]
if before is None:
self.before = None
elif isinstance(before, datetime.datetime):
if joined:
self.before = str(time_snowflake(before, high=False))
else:
self.before = before.isoformat()
else:
if joined:
self.before = str(before.id)
else:
self.before = snowflake_time(before.id).isoformat()
self.update_before: Callable[[ThreadPayload], str] = self.get_archive_timestamp
if joined:
self.endpoint = self.http.get_joined_private_archived_threads
self.update_before = self.get_thread_id
elif private:
self.endpoint = self.http.get_private_archived_threads
else:
self.endpoint = self.http.get_public_archived_threads
self.queue: asyncio.Queue[Thread] = asyncio.Queue()
self.has_more: bool = True
async def next(self) -> Thread:
if self.queue.empty():
await self.fill_queue()
try:
return self.queue.get_nowait()
except asyncio.QueueEmpty:
raise NoMoreItems()
@staticmethod
def get_archive_timestamp(data: ThreadPayload) -> str:
return data["thread_metadata"]["archive_timestamp"]
@staticmethod
def get_thread_id(data: ThreadPayload) -> str:
return data["id"] # type: ignore
async def fill_queue(self) -> None:
if not self.has_more:
raise NoMoreItems()
limit = 50 if self.limit is None else max(self.limit, 50)
data = await self.endpoint(self.channel_id, before=self.before, limit=limit)
# This stuff is obviously WIP because 'members' is always empty
threads: List[ThreadPayload] = data.get("threads", [])
for d in reversed(threads):
self.queue.put_nowait(self.create_thread(d))
self.has_more = data.get("has_more", False)
if self.limit is not None:
self.limit -= len(threads)
if self.limit <= 0:
self.has_more = False
if self.has_more:
self.before = self.update_before(threads[-1])
def create_thread(self, data: ThreadPayload) -> Thread:
from .threads import Thread
return Thread(guild=self.guild, state=self.guild._state, data=data)

View File

@ -22,28 +22,53 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import datetime
import inspect
import itertools
import sys
from operator import attrgetter
from typing import Any, Dict, List, Literal, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union, overload
import discord.abc
from . import utils
from .errors import ClientException
from .user import BaseUser, User
from .activity import create_activity
from .asset import Asset
from .utils import MISSING
from .user import BaseUser, User, _UserTag
from .activity import create_activity, ActivityTypes
from .permissions import Permissions
from .enums import Status, try_enum
from .colour import Colour
from .object import Object
__all__ = (
'VoiceState',
'Member',
"VoiceState",
"Member",
)
if TYPE_CHECKING:
from .asset import Asset
from .channel import DMChannel, VoiceChannel, StageChannel
from .flags import PublicUserFlags
from .guild import Guild
from .types.activity import PartialPresenceUpdate
from .types.member import (
MemberWithUser as MemberWithUserPayload,
Member as MemberPayload,
UserWithMember as UserWithMemberPayload,
)
from .types.user import User as UserPayload
from .abc import Snowflake
from .state import ConnectionState
from .message import Message
from .role import Role
from .types.voice import VoiceState as VoiceStatePayload
VocalGuildChannel = Union[VoiceChannel, StageChannel]
class VoiceState:
"""Represents a Discord user's voice state.
@ -87,42 +112,55 @@ class VoiceState:
is not currently in a voice channel.
"""
__slots__ = ('session_id', 'deaf', 'mute', 'self_mute',
'self_stream', 'self_video', 'self_deaf', 'afk', 'channel',
'requested_to_speak_at', 'suppress')
__slots__ = (
"session_id",
"deaf",
"mute",
"self_mute",
"self_stream",
"self_video",
"self_deaf",
"afk",
"channel",
"requested_to_speak_at",
"suppress",
)
def __init__(self, *, data, channel=None):
self.session_id = data.get('session_id')
def __init__(self, *, data: VoiceStatePayload, channel: Optional[VocalGuildChannel] = None):
self.session_id: str = 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.self_stream = data.get('self_stream', False)
self.self_video = data.get('self_video', False)
self.afk = data.get('suppress', False)
self.mute = data.get('mute', False)
self.deaf = data.get('deaf', False)
self.suppress = data.get('suppress', False)
self.requested_to_speak_at = utils.parse_time(data.get('request_to_speak_timestamp'))
self.channel = channel
def _update(self, data: VoiceStatePayload, channel: Optional[VocalGuildChannel]):
self.self_mute: bool = data.get("self_mute", False)
self.self_deaf: bool = data.get("self_deaf", False)
self.self_stream: bool = data.get("self_stream", False)
self.self_video: bool = data.get("self_video", False)
self.afk: bool = data.get("suppress", False)
self.mute: bool = data.get("mute", False)
self.deaf: bool = data.get("deaf", False)
self.suppress: bool = data.get("suppress", False)
self.requested_to_speak_at: Optional[datetime.datetime] = utils.parse_time(
data.get("request_to_speak_timestamp")
)
self.channel: Optional[VocalGuildChannel] = channel
def __repr__(self):
def __repr__(self) -> str:
attrs = [
('self_mute', self.self_mute),
('self_deaf', self.self_deaf),
('self_stream', self.self_stream),
('suppress', self.suppress),
('requested_to_speak_at', self.requested_to_speak_at),
('channel', self.channel)
("self_mute", self.self_mute),
("self_deaf", self.self_deaf),
("self_stream", self.self_stream),
("suppress", self.suppress),
("requested_to_speak_at", self.requested_to_speak_at),
("channel", self.channel),
]
inner = ' '.join('%s=%r' % t for t in attrs)
return f'<{self.__class__.__name__} {inner}>'
inner = " ".join("%s=%r" % t for t in attrs)
return f"<{self.__class__.__name__} {inner}>"
def flatten_user(cls):
for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()):
# ignore private/special methods
if attr.startswith('_'):
if attr.startswith("_"):
continue
# don't override what we already have
@ -131,9 +169,9 @@ def flatten_user(cls):
# 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__'):
getter = attrgetter('_user.' + attr)
setattr(cls, attr, property(getter, doc=f'Equivalent to :attr:`User.{attr}`'))
if not hasattr(value, "__annotations__"):
getter = attrgetter("_user." + attr)
setattr(cls, attr, property(getter, doc=f"Equivalent to :attr:`User.{attr}`"))
else:
# Technically, this can also use attrgetter
# However I'm not sure how I feel about "functions" returning properties
@ -142,9 +180,12 @@ def flatten_user(cls):
def generate_function(x):
# We want sphinx to properly show coroutine functions as coroutines
if inspect.iscoroutinefunction(value):
async def general(self, *args, **kwargs):
async def general(self, *args, **kwargs): # type: ignore
return await getattr(self._user, x)(*args, **kwargs)
else:
def general(self, *args, **kwargs):
return getattr(self._user, x)(*args, **kwargs)
@ -157,10 +198,12 @@ def flatten_user(cls):
return cls
_BaseUser = discord.abc.User
M = TypeVar("M", bound="Member")
@flatten_user
class Member(discord.abc.Messageable, _BaseUser):
class Member(discord.abc.Messageable, _UserTag):
"""Represents a Discord member to a :class:`Guild`.
This implements a lot of the functionality of :class:`User`.
@ -185,6 +228,10 @@ class Member(discord.abc.Messageable, _BaseUser):
Returns the member's name with the discriminator.
.. describe:: int(x)
Returns the user's ID.
Attributes
----------
joined_at: Optional[:class:`datetime.datetime`]
@ -192,6 +239,13 @@ class Member(discord.abc.Messageable, _BaseUser):
If the member left and rejoined the guild, this will be the latest date. In certain cases, this can be ``None``.
activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]
The activities that the user is currently doing.
.. note::
Due to a Discord API limitation, a user's Spotify activity may not appear
if they are listening to a song with a title longer
than 128 characters. See :issue:`1738` for more information.
guild: :class:`Guild`
The guild that the member belongs to.
nick: Optional[:class:`str`]
@ -202,80 +256,103 @@ class Member(discord.abc.Messageable, _BaseUser):
.. versionadded:: 1.6
premium_since: Optional[:class:`datetime.datetime`]
An aware datetime object that specifies the date and time in UTC when the member used their
Nitro boost on the guild, if available. This could be ``None``.
"Nitro boost" on the guild, if available. This could be ``None``.
"""
__slots__ = ('_roles', 'joined_at', 'premium_since', '_client_status',
'activities', 'guild', 'pending', 'nick', '_user', '_state')
__slots__ = (
"_roles",
"joined_at",
"premium_since",
"activities",
"guild",
"pending",
"nick",
"_client_status",
"_user",
"_state",
"_avatar",
)
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.premium_since = utils.parse_time(data.get('premium_since'))
self._update_roles(data)
self._client_status = {
None: 'offline'
}
self.activities = tuple(map(create_activity, data.get('activities', [])))
self.nick = data.get('nick', None)
self.pending = data.get('pending', False)
if TYPE_CHECKING:
name: str
id: int
discriminator: str
bot: bool
system: bool
created_at: datetime.datetime
default_avatar: Asset
avatar: Optional[Asset]
dm_channel: Optional[DMChannel]
create_dm = User.create_dm
mutual_guilds: List[Guild]
public_flags: PublicUserFlags
banner: Optional[Asset]
accent_color: Optional[Colour]
accent_colour: Optional[Colour]
def __str__(self):
def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: ConnectionState):
self._state: ConnectionState = state
self._user: User = state.store_user(data["user"])
self.guild: Guild = guild
self.joined_at: Optional[datetime.datetime] = utils.parse_time(data.get("joined_at"))
self.premium_since: Optional[datetime.datetime] = utils.parse_time(data.get("premium_since"))
self._roles: utils.SnowflakeList = utils.SnowflakeList(map(int, data["roles"]))
self._client_status: Dict[Optional[str], str] = {None: "offline"}
self.activities: Tuple[ActivityTypes, ...] = tuple()
self.nick: Optional[str] = data.get("nick", None)
self.pending: bool = data.get("pending", False)
self._avatar: Optional[str] = data.get("avatar")
def __str__(self) -> str:
return str(self._user)
def __repr__(self):
return f'<Member id={self._user.id} name={self._user.name!r} discriminator={self._user.discriminator!r}' \
f' bot={self._user.bot} nick={self.nick!r} guild={self.guild!r}>'
def __int__(self) -> int:
return self.id
def __eq__(self, other):
return isinstance(other, _BaseUser) and other.id == self.id
def __repr__(self) -> str:
return (
f"<Member id={self._user.id} name={self._user.name!r} discriminator={self._user.discriminator!r}"
f" bot={self._user.bot} nick={self.nick!r} guild={self.guild!r}>"
)
def __ne__(self, other):
def __eq__(self, other: Any) -> bool:
return isinstance(other, _UserTag) and other.id == self.id
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __hash__(self):
def __hash__(self) -> int:
return hash(self._user)
@classmethod
def _from_message(cls, *, message, data):
def _from_message(cls: Type[M], *, message: Message, data: MemberPayload) -> M:
author = message.author
data['user'] = author._to_minimal_user_json()
return cls(data=data, guild=message.guild, state=message._state)
data["user"] = author._to_minimal_user_json() # type: ignore
return cls(data=data, guild=message.guild, state=message._state) # type: ignore
def _update_from_message(self, data):
self.joined_at = utils.parse_time(data.get('joined_at'))
self.premium_since = utils.parse_time(data.get('premium_since'))
self._update_roles(data)
self.nick = data.get('nick', None)
self.pending = data.get('pending', False)
def _update_from_message(self, data: MemberPayload) -> None:
self.joined_at = utils.parse_time(data.get("joined_at"))
self.premium_since = utils.parse_time(data.get("premium_since"))
self._roles = utils.SnowflakeList(map(int, data["roles"]))
self.nick = data.get("nick", None)
self.pending = data.get("pending", False)
@classmethod
def _try_upgrade(cls, *, data, guild, state):
def _try_upgrade(
cls: Type[M], *, data: UserWithMemberPayload, guild: Guild, state: ConnectionState
) -> Union[User, M]:
# A User object with a 'member' key
try:
member_data = data.pop('member')
member_data = data.pop("member")
except KeyError:
return state.store_user(data)
return state.create_user(data)
else:
member_data['user'] = data
return cls(data=member_data, guild=guild, state=state)
member_data["user"] = data # type: ignore
return cls(data=member_data, guild=guild, state=state) # type: ignore
@classmethod
def _from_presence_update(cls, *, data, guild, state):
clone = cls(data=data, guild=guild, state=state)
to_return = cls(data=data, guild=guild, state=state)
to_return._client_status = {
sys.intern(key): sys.intern(value)
for key, value in data.get('client_status', {}).items()
}
to_return._client_status[None] = sys.intern(data['status'])
return to_return, clone
@classmethod
def _copy(cls, member):
self = cls.__new__(cls) # to bypass __init__
def _copy(cls: Type[M], member: M) -> M:
self: M = cls.__new__(cls) # to bypass __init__
self._roles = utils.SnowflakeList(member._roles, is_sorted=True)
self.joined_at = member.joined_at
@ -286,6 +363,7 @@ class Member(discord.abc.Messageable, _BaseUser):
self.pending = member.pending
self.activities = member.activities
self._state = member._state
self._avatar = member._avatar
# Reference will not be copied unless necessary by PRESENCE_UPDATE
# See below
@ -296,55 +374,52 @@ class Member(discord.abc.Messageable, _BaseUser):
ch = await self.create_dm()
return ch
def _update_roles(self, data):
self._roles = utils.SnowflakeList(map(int, data['roles']))
def _update(self, data):
def _update(self, data: MemberPayload) -> None:
# the nickname change is optional,
# if it isn't in the payload then it didn't change
try:
self.nick = data['nick']
self.nick = data["nick"]
except KeyError:
pass
try:
self.pending = data['pending']
self.pending = data["pending"]
except KeyError:
pass
self.premium_since = utils.parse_time(data.get('premium_since'))
self._update_roles(data)
self.premium_since = utils.parse_time(data.get("premium_since"))
self._roles = utils.SnowflakeList(map(int, data["roles"]))
self._avatar = data.get("avatar")
def _presence_update(self, data, user):
self.activities = tuple(map(create_activity, data.get('activities', [])))
def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]:
self.activities = tuple(map(create_activity, data["activities"]))
self._client_status = {
sys.intern(key): sys.intern(value)
for key, value in data.get('client_status', {}).items()
sys.intern(key): sys.intern(value) for key, value in data.get("client_status", {}).items() # type: ignore
}
self._client_status[None] = sys.intern(data['status'])
self._client_status[None] = sys.intern(data["status"])
if len(user) > 1:
return self._update_inner_user(user)
return False
return None
def _update_inner_user(self, user):
def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]:
u = self._user
original = (u.name, u.avatar, u.discriminator, u._public_flags)
original = (u.name, u._avatar, u.discriminator, u._public_flags)
# These keys seem to always be available
modified = (user['username'], user['avatar'], user['discriminator'], user.get('public_flags', 0))
modified = (user["username"], user["avatar"], user["discriminator"], user.get("public_flags", 0))
if original != modified:
to_return = User._copy(self._user)
u.name, u.avatar, u.discriminator, u._public_flags = modified
u.name, u._avatar, u.discriminator, u._public_flags = modified
# Signal to dispatch on_user_update
return to_return, u
@property
def status(self):
def status(self) -> Status:
""":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])
@property
def raw_status(self):
def raw_status(self) -> str:
""":class:`str`: The member's overall status as a string value.
.. versionadded:: 1.5
@ -352,31 +427,31 @@ class Member(discord.abc.Messageable, _BaseUser):
return self._client_status[None]
@status.setter
def status(self, value):
def status(self, value: Status) -> None:
# internal use only
self._client_status[None] = str(value)
@property
def mobile_status(self):
def mobile_status(self) -> Status:
""":class:`Status`: The member's status on a mobile device, if applicable."""
return try_enum(Status, self._client_status.get('mobile', 'offline'))
return try_enum(Status, self._client_status.get("mobile", "offline"))
@property
def desktop_status(self):
def desktop_status(self) -> Status:
""":class:`Status`: The member's status on the desktop client, if applicable."""
return try_enum(Status, self._client_status.get('desktop', 'offline'))
return try_enum(Status, self._client_status.get("desktop", "offline"))
@property
def web_status(self):
def web_status(self) -> Status:
""":class:`Status`: The member's status on the web client, if applicable."""
return try_enum(Status, self._client_status.get('web', 'offline'))
return try_enum(Status, self._client_status.get("web", "offline"))
def is_on_mobile(self):
def is_on_mobile(self) -> bool:
""":class:`bool`: A helper function that determines if a member is active on a mobile device."""
return 'mobile' in self._client_status
return "mobile" in self._client_status
@property
def colour(self):
def colour(self) -> Colour:
""":class:`Colour`: A property that returns a 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.
@ -384,7 +459,7 @@ class Member(discord.abc.Messageable, _BaseUser):
There is an alias for this named :attr:`color`.
"""
roles = self.roles[1:] # remove @everyone
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
@ -395,7 +470,7 @@ class Member(discord.abc.Messageable, _BaseUser):
return Colour.default()
@property
def color(self):
def color(self) -> Colour:
""":class:`Colour`: A property that returns a color denoting the rendered color for
the member. If the default color is the one rendered then an instance of :meth:`Colour.default`
is returned.
@ -405,7 +480,7 @@ class Member(discord.abc.Messageable, _BaseUser):
return self.colour
@property
def roles(self):
def roles(self) -> List[Role]:
"""List[:class:`Role`]: 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.
@ -423,14 +498,14 @@ class Member(discord.abc.Messageable, _BaseUser):
return result
@property
def mention(self):
def mention(self) -> str:
""":class:`str`: Returns a string that allows you to mention the member."""
if self.nick:
return f'<@!{self._user.id}>'
return f'<@{self._user.id}>'
return f"<@!{self._user.id}>"
return f"<@{self._user.id}>"
@property
def display_name(self):
def display_name(self) -> str:
""":class:`str`: Returns the user's display name.
For regular users this is just their username, but
@ -440,10 +515,39 @@ class Member(discord.abc.Messageable, _BaseUser):
return self.nick or self.name
@property
def activity(self):
"""Union[:class:`BaseActivity`, :class:`Spotify`]: Returns the primary
def display_avatar(self) -> Asset:
""":class:`Asset`: Returns the member's display avatar.
For regular members this is just their avatar, but
if they have a guild specific avatar then that
is returned instead.
.. versionadded:: 2.0
"""
return self.guild_avatar or self._user.avatar or self._user.default_avatar
@property
def guild_avatar(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns an :class:`Asset` for the guild avatar
the member has. If unavailable, ``None`` is returned.
.. versionadded:: 2.0
"""
if self._avatar is None:
return None
return Asset._from_guild_avatar(self._state, self.guild.id, self.id, self._avatar)
@property
def activity(self) -> Optional[ActivityTypes]:
"""Optional[Union[:class:`BaseActivity`, :class:`Spotify`]]: Returns the primary
activity the user is currently doing. Could be ``None`` if no activity is being done.
.. note::
Due to a Discord API limitation, this may be ``None`` if
the user is listening to a song on Spotify with a title longer
than 128 characters. See :issue:`1738` for more information.
.. note::
A user may have multiple activities, these can be accessed under :attr:`activities`.
@ -451,7 +555,7 @@ class Member(discord.abc.Messageable, _BaseUser):
if self.activities:
return self.activities[0]
def mentioned_in(self, message):
def mentioned_in(self, message: Message) -> bool:
"""Checks if the member is mentioned in the specified message.
Parameters
@ -472,29 +576,8 @@ class Member(discord.abc.Messageable, _BaseUser):
return any(self._roles.has(role.id) for role in message.role_mentions)
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: :class:`abc.GuildChannel`
The channel to check your permissions for.
Returns
-------
:class:`Permissions`
The resolved permissions for the member.
"""
return channel.permissions_for(self)
@property
def top_role(self):
def top_role(self) -> Role:
""":class:`Role`: Returns the member's highest role.
This is useful for figuring where a member stands in the role
@ -507,14 +590,13 @@ class Member(discord.abc.Messageable, _BaseUser):
return max(guild.get_role(rid) or guild.default_role for rid in self._roles)
@property
def guild_permissions(self):
def guild_permissions(self) -> Permissions:
""":class:`Permissions`: 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`.
calculation, please use :meth:`abc.GuildChannel.permissions_for`.
This does take into consideration guild ownership and the
administrator implication.
@ -533,32 +615,47 @@ class Member(discord.abc.Messageable, _BaseUser):
return base
@property
def voice(self):
def voice(self) -> Optional[VoiceState]:
"""Optional[:class:`VoiceState`]: Returns the member's current voice state."""
return self.guild._voice_state_for(self._user.id)
async def ban(self, **kwargs):
async def ban(
self,
*,
delete_message_days: Literal[0, 1, 2, 3, 4, 5, 6, 7] = 1,
reason: Optional[str] = None,
) -> None:
"""|coro|
Bans this member. Equivalent to :meth:`Guild.ban`.
"""
await self.guild.ban(self, **kwargs)
await self.guild.ban(self, reason=reason, delete_message_days=delete_message_days)
async def unban(self, *, reason=None):
async def unban(self, *, reason: Optional[str] = None) -> None:
"""|coro|
Unbans this member. Equivalent to :meth:`Guild.unban`.
"""
await self.guild.unban(self, reason=reason)
async def kick(self, *, reason=None):
async def kick(self, *, reason: Optional[str] = None) -> None:
"""|coro|
Kicks this member. Equivalent to :meth:`Guild.kick`.
"""
await self.guild.kick(self, reason=reason)
async def edit(self, *, reason=None, **fields):
async def edit(
self,
*,
nick: Optional[str] = MISSING,
mute: bool = MISSING,
deafen: bool = MISSING,
suppress: bool = MISSING,
roles: List[discord.abc.Snowflake] = MISSING,
voice_channel: Optional[VocalGuildChannel] = MISSING,
reason: Optional[str] = None,
) -> Optional[Member]:
"""|coro|
Edits the member's data.
@ -584,6 +681,9 @@ class Member(discord.abc.Messageable, _BaseUser):
.. versionchanged:: 1.1
Can now pass ``None`` to ``voice_channel`` to kick a member from voice.
.. versionchanged:: 2.0
The newly member is now optionally returned, if applicable.
Parameters
-----------
nick: Optional[:class:`str`]
@ -597,7 +697,7 @@ class Member(discord.abc.Messageable, _BaseUser):
.. versionadded:: 1.7
roles: Optional[List[:class:`Role`]]
roles: List[:class:`Role`]
The member's new list of roles. This *replaces* the roles.
voice_channel: Optional[:class:`VoiceChannel`]
The voice channel to move the member to.
@ -611,69 +711,58 @@ class Member(discord.abc.Messageable, _BaseUser):
You do not have the proper permissions to the action requested.
HTTPException
The operation failed.
Returns
--------
Optional[:class:`.Member`]
The newly updated member, if applicable. This is only returned
when certain fields are updated.
"""
http = self._state.http
guild_id = self.guild.id
me = self._state.self_id == self.id
payload = {}
payload: Dict[str, Any] = {}
try:
nick = fields['nick']
except KeyError:
# nick not present so...
pass
else:
nick = nick or ''
if nick is not MISSING:
nick = nick or ""
if me:
await http.change_my_nickname(guild_id, nick, reason=reason)
else:
payload['nick'] = nick
payload["nick"] = nick
deafen = fields.get('deafen')
if deafen is not None:
payload['deaf'] = deafen
if deafen is not MISSING:
payload["deaf"] = deafen
mute = fields.get('mute')
if mute is not None:
payload['mute'] = mute
if mute is not MISSING:
payload["mute"] = mute
suppress = fields.get('suppress')
if suppress is not None:
if suppress is not MISSING:
voice_state_payload = {
'channel_id': self.voice.channel.id,
'suppress': suppress,
"channel_id": self.voice.channel.id,
"suppress": suppress,
}
if suppress or self.bot:
voice_state_payload['request_to_speak_timestamp'] = None
voice_state_payload["request_to_speak_timestamp"] = None
if me:
await http.edit_my_voice_state(guild_id, voice_state_payload)
else:
if not suppress:
voice_state_payload['request_to_speak_timestamp'] = datetime.datetime.utcnow().isoformat()
voice_state_payload["request_to_speak_timestamp"] = datetime.datetime.utcnow().isoformat()
await http.edit_voice_state(guild_id, self.id, voice_state_payload)
try:
vc = fields['voice_channel']
except KeyError:
pass
else:
payload['channel_id'] = vc and vc.id
if voice_channel is not MISSING:
payload["channel_id"] = voice_channel and voice_channel.id
try:
roles = fields['roles']
except KeyError:
pass
else:
payload['roles'] = tuple(r.id for r in roles)
if roles is not MISSING:
payload["roles"] = tuple(r.id for r in roles)
if payload:
await http.edit_member(guild_id, self.id, reason=reason, **payload)
data = await http.edit_member(guild_id, self.id, reason=reason, **payload)
return Member(data=data, guild=self.guild, state=self._state)
# TODO: wait for WS event for modify-in-place behaviour
async def request_to_speak(self):
async def request_to_speak(self) -> None:
"""|coro|
Request to speak in the connected channel.
@ -695,17 +784,17 @@ class Member(discord.abc.Messageable, _BaseUser):
The operation failed.
"""
payload = {
'channel_id': self.voice.channel.id,
'request_to_speak_timestamp': datetime.datetime.utcnow().isoformat(),
"channel_id": self.voice.channel.id,
"request_to_speak_timestamp": datetime.datetime.utcnow().isoformat(),
}
if self._state.self_id != self.id:
payload['suppress'] = False
payload["suppress"] = False
await self._state.http.edit_voice_state(self.guild.id, self.id, payload)
else:
await self._state.http.edit_my_voice_state(self.guild.id, payload)
async def move_to(self, channel, *, reason=None):
async def move_to(self, channel: VocalGuildChannel, *, reason: Optional[str] = None) -> None:
"""|coro|
Moves a member to a new voice channel (they must be connected first).
@ -728,7 +817,7 @@ class Member(discord.abc.Messageable, _BaseUser):
"""
await self.edit(voice_channel=channel, reason=reason)
async def add_roles(self, *roles, reason=None, atomic=True):
async def add_roles(self, *roles: Snowflake, reason: Optional[str] = None, atomic: bool = True) -> None:
r"""|coro|
Gives the member a number of :class:`Role`\s.
@ -767,7 +856,7 @@ class Member(discord.abc.Messageable, _BaseUser):
for role in roles:
await req(guild_id, user_id, role.id, reason=reason)
async def remove_roles(self, *roles, reason=None, atomic=True):
async def remove_roles(self, *roles: Snowflake, reason: Optional[str] = None, atomic: bool = True) -> None:
r"""|coro|
Removes :class:`Role`\s from this member.
@ -797,7 +886,7 @@ class Member(discord.abc.Messageable, _BaseUser):
"""
if not atomic:
new_roles = [Object(id=r.id) for r in self.roles[1:]] # remove @everyone
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))
@ -811,3 +900,20 @@ class Member(discord.abc.Messageable, _BaseUser):
user_id = self.id
for role in roles:
await req(guild_id, user_id, role.id, reason=reason)
def get_role(self, role_id: int, /) -> Optional[Role]:
"""Returns a role with the given ID from roles which the member has.
.. versionadded:: 2.0
Parameters
-----------
role_id: :class:`int`
The ID to search for.
Returns
--------
Optional[:class:`Role`]
The role or ``None`` if not found in the member's roles.
"""
return self.guild.get_role(role_id) if self._roles.has(role_id) else None

View File

@ -22,13 +22,19 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
__all__ = (
'AllowedMentions',
)
from __future__ import annotations
from typing import Type, TypeVar, Union, List, TYPE_CHECKING, Any, Union
__all__ = ("AllowedMentions",)
if TYPE_CHECKING:
from .types.message import AllowedMentions as AllowedMentionsPayload
from .abc import Snowflake
class _FakeBool:
def __repr__(self):
return 'True'
return "True"
def __eq__(self, other):
return other is True
@ -36,7 +42,11 @@ class _FakeBool:
def __bool__(self):
return True
default = _FakeBool()
default: Any = _FakeBool()
A = TypeVar("A", bound="AllowedMentions")
class AllowedMentions:
"""A class that represents what mentions are allowed in a message.
@ -68,16 +78,23 @@ class AllowedMentions:
.. versionadded:: 1.6
"""
__slots__ = ('everyone', 'users', 'roles', 'replied_user')
__slots__ = ("everyone", "users", "roles", "replied_user")
def __init__(self, *, everyone=default, users=default, roles=default, replied_user=default):
def __init__(
self,
*,
everyone: bool = default,
users: Union[bool, List[Snowflake]] = default,
roles: Union[bool, List[Snowflake]] = default,
replied_user: bool = default,
):
self.everyone = everyone
self.users = users
self.roles = roles
self.replied_user = replied_user
@classmethod
def all(cls):
def all(cls: Type[A]) -> A:
"""A factory method that returns a :class:`AllowedMentions` with all fields explicitly set to ``True``
.. versionadded:: 1.5
@ -85,37 +102,37 @@ class AllowedMentions:
return cls(everyone=True, users=True, roles=True, replied_user=True)
@classmethod
def none(cls):
def none(cls: Type[A]) -> A:
"""A factory method that returns a :class:`AllowedMentions` with all fields set to ``False``
.. versionadded:: 1.5
"""
return cls(everyone=False, users=False, roles=False, replied_user=False)
def to_dict(self):
def to_dict(self) -> AllowedMentionsPayload:
parse = []
data = {}
if self.everyone:
parse.append('everyone')
parse.append("everyone")
if self.users == True:
parse.append('users')
parse.append("users")
elif self.users != False:
data['users'] = [x.id for x in self.users]
data["users"] = [x.id for x in self.users]
if self.roles == True:
parse.append('roles')
parse.append("roles")
elif self.roles != False:
data['roles'] = [x.id for x in self.roles]
data["roles"] = [x.id for x in self.roles]
if self.replied_user:
data['replied_user'] = True
data["replied_user"] = True
data['parse'] = parse
return data
data["parse"] = parse
return data # type: ignore
def merge(self, other):
def merge(self, other: AllowedMentions) -> AllowedMentions:
# Creates a new AllowedMentions by merging from another one.
# Merge is done by using the 'self' values unless explicitly
# overridden by the 'other' values.
@ -125,5 +142,8 @@ class AllowedMentions:
replied_user = self.replied_user if other.replied_user is default else other.replied_user
return AllowedMentions(everyone=everyone, roles=roles, users=users, replied_user=replied_user)
def __repr__(self):
return '{0.__class__.__qualname__}(everyone={0.everyone}, users={0.users}, roles={0.roles}, replied_user={0.replied_user})'.format(self)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}(everyone={self.everyone}, "
f"users={self.users}, roles={self.roles}, replied_user={self.replied_user})"
)

File diff suppressed because it is too large Load Diff

View File

@ -23,23 +23,30 @@ DEALINGS IN THE SOFTWARE.
"""
__all__ = (
'EqualityComparable',
'Hashable',
"EqualityComparable",
"Hashable",
)
class EqualityComparable:
__slots__ = ()
def __eq__(self, other):
id: int
def __eq__(self, other: object) -> bool:
return isinstance(other, self.__class__) and other.id == self.id
def __ne__(self, other):
def __ne__(self, other: object) -> bool:
if isinstance(other, self.__class__):
return other.id != self.id
return True
class Hashable(EqualityComparable):
__slots__ = ()
def __hash__(self):
def __int__(self) -> int:
return self.id
def __hash__(self) -> int:
return self.id >> 22

View File

@ -22,13 +22,25 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from . import utils
from .mixins import Hashable
__all__ = (
'Object',
from typing import (
SupportsInt,
TYPE_CHECKING,
Union,
)
if TYPE_CHECKING:
import datetime
SupportsIntCast = Union[SupportsInt, str, bytes, bytearray]
__all__ = ("Object",)
class Object(Hashable):
"""Represents a generic Discord object.
@ -57,24 +69,28 @@ class Object(Hashable):
Returns the object's hash.
.. describe:: int(x)
Returns the object's ID.
Attributes
-----------
id: :class:`int`
The ID of the object.
"""
def __init__(self, id):
def __init__(self, id: SupportsIntCast):
try:
id = int(id)
except ValueError:
raise TypeError(f'id parameter must be convertable to int not {id.__class__!r}') from None
raise TypeError(f"id parameter must be convertable to int not {id.__class__!r}") from None
else:
self.id = id
def __repr__(self):
return f'<Object id={self.id!r}>'
def __repr__(self) -> str:
return f"<Object id={self.id!r}>"
@property
def created_at(self):
def created_at(self) -> datetime.datetime:
""":class:`datetime.datetime`: Returns the snowflake's creation time in UTC."""
return utils.snowflake_time(self.id)

View File

@ -22,40 +22,54 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import struct
from typing import TYPE_CHECKING, ClassVar, IO, Generator, Tuple, Optional
from .errors import DiscordException
__all__ = (
'OggError',
'OggPage',
'OggStream',
"OggError",
"OggPage",
"OggStream",
)
class OggError(DiscordException):
"""An exception that is thrown for Ogg stream parsing errors."""
pass
# https://tools.ietf.org/html/rfc3533
# https://tools.ietf.org/html/rfc7845
class OggPage:
_header = struct.Struct('<xBQIIIB')
def __init__(self, stream):
class OggPage:
_header: ClassVar[struct.Struct] = struct.Struct("<xBQIIIB")
if TYPE_CHECKING:
flag: int
gran_pos: int
serial: int
pagenum: int
crc: int
segnum: int
def __init__(self, stream: IO[bytes]) -> None:
try:
header = stream.read(struct.calcsize(self._header.format))
self.flag, self.gran_pos, self.serial, \
self.pagenum, self.crc, self.segnum = self._header.unpack(header)
self.flag, self.gran_pos, self.serial, self.pagenum, self.crc, self.segnum = self._header.unpack(header)
self.segtable = stream.read(self.segnum)
bodylen = sum(struct.unpack('B'*self.segnum, self.segtable))
self.data = stream.read(bodylen)
self.segtable: bytes = stream.read(self.segnum)
bodylen = sum(struct.unpack("B" * self.segnum, self.segtable))
self.data: bytes = stream.read(bodylen)
except Exception:
raise OggError('bad data stream') from None
raise OggError("bad data stream") from None
def iter_packets(self):
def iter_packets(self) -> Generator[Tuple[bytes, bool], None, None]:
packetlen = offset = 0
partial = True
@ -65,7 +79,7 @@ class OggPage:
partial = True
else:
packetlen += seg
yield self.data[offset:offset+packetlen], True
yield self.data[offset : offset + packetlen], True
offset += packetlen
packetlen = 0
partial = False
@ -73,30 +87,31 @@ class OggPage:
if partial:
yield self.data[offset:], False
class OggStream:
def __init__(self, stream):
self.stream = stream
def _next_page(self):
class OggStream:
def __init__(self, stream: IO[bytes]) -> None:
self.stream: IO[bytes] = stream
def _next_page(self) -> Optional[OggPage]:
head = self.stream.read(4)
if head == b'OggS':
if head == b"OggS":
return OggPage(self.stream)
elif not head:
return None
else:
raise OggError('invalid header magic')
raise OggError("invalid header magic")
def _iter_pages(self):
def _iter_pages(self) -> Generator[OggPage, None, None]:
page = self._next_page()
while page:
yield page
page = self._next_page()
def iter_packets(self):
partial = b''
def iter_packets(self) -> Generator[bytes, None, None]:
partial = b""
for page in self._iter_pages():
for data, complete in page.iter_packets():
partial += data
if complete:
yield partial
partial = b''
partial = b""

View File

@ -22,6 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import List, Tuple, TypedDict, Any, TYPE_CHECKING, Callable, TypeVar, Literal, Optional, overload
import array
import ctypes
import ctypes.util
@ -31,134 +35,157 @@ import os.path
import struct
import sys
from .errors import DiscordException
from .errors import DiscordException, InvalidArgument
if TYPE_CHECKING:
T = TypeVar("T")
BAND_CTL = Literal["narrow", "medium", "wide", "superwide", "full"]
SIGNAL_CTL = Literal["auto", "voice", "music"]
class BandCtl(TypedDict):
narrow: int
medium: int
wide: int
superwide: int
full: int
class SignalCtl(TypedDict):
auto: int
voice: int
music: int
__all__ = (
'Encoder',
'OpusError',
'OpusNotLoaded',
"Encoder",
"OpusError",
"OpusNotLoaded",
)
log = logging.getLogger(__name__)
_log = logging.getLogger(__name__)
c_int_ptr = ctypes.POINTER(ctypes.c_int)
c_int_ptr = ctypes.POINTER(ctypes.c_int)
c_int16_ptr = ctypes.POINTER(ctypes.c_int16)
c_float_ptr = ctypes.POINTER(ctypes.c_float)
_lib = None
class EncoderStruct(ctypes.Structure):
pass
class DecoderStruct(ctypes.Structure):
pass
EncoderStructPtr = ctypes.POINTER(EncoderStruct)
DecoderStructPtr = ctypes.POINTER(DecoderStruct)
## Some constants from opus_defines.h
# Error codes
OK = 0
OK = 0
BAD_ARG = -1
# Encoder CTLs
APPLICATION_AUDIO = 2049
APPLICATION_VOIP = 2048
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
CTL_SET_BITRATE = 4002
CTL_SET_BANDWIDTH = 4008
CTL_SET_FEC = 4012
CTL_SET_PLP = 4014
CTL_SET_SIGNAL = 4024
# Decoder CTLs
CTL_SET_GAIN = 4034
CTL_SET_GAIN = 4034
CTL_LAST_PACKET_DURATION = 4039
band_ctl = {
'narrow': 1101,
'medium': 1102,
'wide': 1103,
'superwide': 1104,
'full': 1105,
band_ctl: BandCtl = {
"narrow": 1101,
"medium": 1102,
"wide": 1103,
"superwide": 1104,
"full": 1105,
}
signal_ctl = {
'auto': -1000,
'voice': 3001,
'music': 3002,
signal_ctl: SignalCtl = {
"auto": -1000,
"voice": 3001,
"music": 3002,
}
def _err_lt(result, func, args):
def _err_lt(result: int, func: Callable, args: List) -> int:
if result < OK:
log.info('error has happened in %s', func.__name__)
_log.info("error has happened in %s", func.__name__)
raise OpusError(result)
return result
def _err_ne(result, func, args):
def _err_ne(result: T, func: Callable, args: List) -> T:
ret = args[-1]._obj
if ret.value != OK:
log.info('error has happened in %s', func.__name__)
_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 = [
exported_functions: List[Tuple[Any, ...]] = [
# Generic
('opus_get_version_string',
None, ctypes.c_char_p, None),
('opus_strerror',
[ctypes.c_int], ctypes.c_char_p, None),
("opus_get_version_string", None, ctypes.c_char_p, None),
("opus_strerror", [ctypes.c_int], ctypes.c_char_p, None),
# Encoder functions
('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_encode_float',
[EncoderStructPtr, c_float_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),
("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_encode_float",
[EncoderStructPtr, c_float_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),
# Decoder functions
('opus_decoder_get_size',
[ctypes.c_int], ctypes.c_int, None),
('opus_decoder_create',
[ctypes.c_int, ctypes.c_int, c_int_ptr], DecoderStructPtr, _err_ne),
('opus_decode',
("opus_decoder_get_size", [ctypes.c_int], ctypes.c_int, None),
("opus_decoder_create", [ctypes.c_int, ctypes.c_int, c_int_ptr], DecoderStructPtr, _err_ne),
(
"opus_decode",
[DecoderStructPtr, ctypes.c_char_p, ctypes.c_int32, c_int16_ptr, ctypes.c_int, ctypes.c_int],
ctypes.c_int, _err_lt),
('opus_decode_float',
ctypes.c_int,
_err_lt,
),
(
"opus_decode_float",
[DecoderStructPtr, ctypes.c_char_p, ctypes.c_int32, c_float_ptr, ctypes.c_int, ctypes.c_int],
ctypes.c_int, _err_lt),
('opus_decoder_ctl',
None, ctypes.c_int32, _err_lt),
('opus_decoder_destroy',
[DecoderStructPtr], None, None),
('opus_decoder_get_nb_samples',
[DecoderStructPtr, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int, _err_lt),
ctypes.c_int,
_err_lt,
),
("opus_decoder_ctl", None, ctypes.c_int32, _err_lt),
("opus_decoder_destroy", [DecoderStructPtr], None, None),
("opus_decoder_get_nb_samples", [DecoderStructPtr, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int, _err_lt),
# Packet functions
('opus_packet_get_bandwidth',
[ctypes.c_char_p], ctypes.c_int, _err_lt),
('opus_packet_get_nb_channels',
[ctypes.c_char_p], ctypes.c_int, _err_lt),
('opus_packet_get_nb_frames',
[ctypes.c_char_p, ctypes.c_int], ctypes.c_int, _err_lt),
('opus_packet_get_samples_per_frame',
[ctypes.c_char_p, ctypes.c_int], ctypes.c_int, _err_lt),
("opus_packet_get_bandwidth", [ctypes.c_char_p], ctypes.c_int, _err_lt),
("opus_packet_get_nb_channels", [ctypes.c_char_p], ctypes.c_int, _err_lt),
("opus_packet_get_nb_frames", [ctypes.c_char_p, ctypes.c_int], ctypes.c_int, _err_lt),
("opus_packet_get_samples_per_frame", [ctypes.c_char_p, ctypes.c_int], ctypes.c_int, _err_lt),
]
def libopus_loader(name):
def libopus_loader(name: str) -> Any:
# create the library...
lib = ctypes.cdll.LoadLibrary(name)
@ -178,27 +205,29 @@ def libopus_loader(name):
if item[3]:
func.errcheck = item[3]
except KeyError:
log.exception("Error assigning check function to %s", func)
_log.exception("Error assigning check function to %s", func)
return lib
def _load_default():
def _load_default() -> bool:
global _lib
try:
if sys.platform == 'win32':
if sys.platform == "win32":
_basedir = os.path.dirname(os.path.abspath(__file__))
_bitness = struct.calcsize('P') * 8
_target = 'x64' if _bitness > 32 else 'x86'
_filename = os.path.join(_basedir, 'bin', f'libopus-0.{_target}.dll')
_bitness = struct.calcsize("P") * 8
_target = "x64" if _bitness > 32 else "x86"
_filename = os.path.join(_basedir, "bin", f"libopus-0.{_target}.dll")
_lib = libopus_loader(_filename)
else:
_lib = libopus_loader(ctypes.util.find_library('opus'))
_lib = libopus_loader(ctypes.util.find_library("opus"))
except Exception:
_lib = None
return _lib is not None
def load_opus(name):
def load_opus(name: str) -> None:
"""Loads the libopus shared library for use with voice.
If this function is not called then the library uses the function
@ -236,7 +265,8 @@ def load_opus(name):
global _lib
_lib = libopus_loader(name)
def is_loaded():
def is_loaded() -> bool:
"""Function to check if opus lib is successfully loaded either
via the :func:`ctypes.util.find_library` call of :func:`load_opus`.
@ -250,6 +280,7 @@ def is_loaded():
global _lib
return _lib is not None
class OpusError(DiscordException):
"""An exception that is thrown for libopus related errors.
@ -259,21 +290,24 @@ class OpusError(DiscordException):
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)
def __init__(self, code: int):
self.code: int = 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
class _OpusStruct:
SAMPLING_RATE = 48000
CHANNELS = 2
FRAME_LENGTH = 20 # in milliseconds
SAMPLE_SIZE = struct.calcsize('h') * CHANNELS
SAMPLE_SIZE = struct.calcsize("h") * CHANNELS
SAMPLES_PER_FRAME = int(SAMPLING_RATE / 1000 * FRAME_LENGTH)
FRAME_SIZE = SAMPLES_PER_FRAME * SAMPLE_SIZE
@ -283,95 +317,101 @@ class _OpusStruct:
if not is_loaded() and not _load_default():
raise OpusNotLoaded()
return _lib.opus_get_version_string().decode('utf-8')
return _lib.opus_get_version_string().decode("utf-8")
class Encoder(_OpusStruct):
def __init__(self, application=APPLICATION_AUDIO):
def __init__(self, application: int = APPLICATION_AUDIO):
_OpusStruct.get_opus_version()
self.application = application
self._state = self._create_state()
self.application: int = application
self._state: EncoderStruct = 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')
self.set_bandwidth("full")
self.set_signal_type("auto")
def __del__(self):
if hasattr(self, '_state'):
def __del__(self) -> None:
if hasattr(self, "_state"):
_lib.opus_encoder_destroy(self._state)
self._state = None
# This is a destructor, so it's okay to assign None
self._state = None # type: ignore
def _create_state(self):
def _create_state(self) -> EncoderStruct:
ret = ctypes.c_int()
return _lib.opus_encoder_create(self.SAMPLING_RATE, self.CHANNELS, self.application, ctypes.byref(ret))
def set_bitrate(self, kbps):
def set_bitrate(self, kbps: int) -> int:
kbps = min(512, max(16, int(kbps)))
_lib.opus_encoder_ctl(self._state, CTL_SET_BITRATE, kbps * 1024)
return kbps
def set_bandwidth(self, req):
def set_bandwidth(self, req: BAND_CTL) -> None:
if req not in band_ctl:
raise KeyError(f'{req!r} is not a valid bandwidth setting. Try one of: {",".join(band_ctl)}')
k = band_ctl[req]
_lib.opus_encoder_ctl(self._state, CTL_SET_BANDWIDTH, k)
def set_signal_type(self, req):
def set_signal_type(self, req: SIGNAL_CTL) -> None:
if req not in signal_ctl:
raise KeyError(f'{req!r} is not a valid bandwidth setting. Try one of: {",".join(signal_ctl)}')
k = signal_ctl[req]
_lib.opus_encoder_ctl(self._state, CTL_SET_SIGNAL, k)
def set_fec(self, enabled=True):
def set_fec(self, enabled: bool = True) -> None:
_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 set_expected_packet_loss_percent(self, percentage: float) -> None:
_lib.opus_encoder_ctl(self._state, CTL_SET_PLP, min(100, max(0, int(percentage * 100)))) # type: ignore
def encode(self, pcm, frame_size):
def encode(self, pcm: bytes, frame_size: int) -> bytes:
max_data_bytes = len(pcm)
pcm = ctypes.cast(pcm, c_int16_ptr)
# bytes can be used to reference pointer
pcm_ptr = ctypes.cast(pcm, c_int16_ptr) # type: ignore
data = (ctypes.c_char * max_data_bytes)()
ret = _lib.opus_encode(self._state, pcm, frame_size, data, max_data_bytes)
ret = _lib.opus_encode(self._state, pcm_ptr, frame_size, data, max_data_bytes)
# array can be initialized with bytes but mypy doesn't know
return array.array("b", data[:ret]).tobytes() # type: ignore
return array.array('b', data[:ret]).tobytes()
class Decoder(_OpusStruct):
def __init__(self):
_OpusStruct.get_opus_version()
self._state = self._create_state()
self._state: DecoderStruct = self._create_state()
def __del__(self):
if hasattr(self, '_state'):
def __del__(self) -> None:
if hasattr(self, "_state"):
_lib.opus_decoder_destroy(self._state)
self._state = None
# This is a destructor, so it's okay to assign None
self._state = None # type: ignore
def _create_state(self):
def _create_state(self) -> DecoderStruct:
ret = ctypes.c_int()
return _lib.opus_decoder_create(self.SAMPLING_RATE, self.CHANNELS, ctypes.byref(ret))
@staticmethod
def packet_get_nb_frames(data):
def packet_get_nb_frames(data: bytes) -> int:
"""Gets the number of frames in an Opus packet"""
return _lib.opus_packet_get_nb_frames(data, len(data))
@staticmethod
def packet_get_nb_channels(data):
def packet_get_nb_channels(data: bytes) -> int:
"""Gets the number of channels in an Opus packet"""
return _lib.opus_packet_get_nb_channels(data)
@classmethod
def packet_get_samples_per_frame(cls, data):
def packet_get_samples_per_frame(cls, data: bytes) -> int:
"""Gets the number of samples per frame from an Opus packet"""
return _lib.opus_packet_get_samples_per_frame(data, cls.SAMPLING_RATE)
def _set_gain(self, adjustment):
def _set_gain(self, adjustment: int) -> int:
"""Configures decoder gain adjustment.
Scales the decoded output by a factor specified in Q8 dB units.
@ -383,26 +423,34 @@ class Decoder(_OpusStruct):
"""
return _lib.opus_decoder_ctl(self._state, CTL_SET_GAIN, adjustment)
def set_gain(self, dB):
def set_gain(self, dB: float) -> int:
"""Sets the decoder gain in dB, from -128 to 128."""
dB_Q8 = max(-32768, min(32767, round(dB * 256))) # dB * 2^n where n is 8 (Q8)
dB_Q8 = max(-32768, min(32767, round(dB * 256))) # dB * 2^n where n is 8 (Q8)
return self._set_gain(dB_Q8)
def set_volume(self, mult):
def set_volume(self, mult: float) -> int:
"""Sets the output volume as a float percent, i.e. 0.5 for 50%, 1.75 for 175%, etc."""
return self.set_gain(20 * math.log10(mult)) # amplitude ratio
return self.set_gain(20 * math.log10(mult)) # amplitude ratio
def _get_last_packet_duration(self):
def _get_last_packet_duration(self) -> int:
"""Gets the duration (in samples) of the last packet successfully decoded or concealed."""
ret = ctypes.c_int32()
_lib.opus_decoder_ctl(self._state, CTL_LAST_PACKET_DURATION, ctypes.byref(ret))
return ret.value
def decode(self, data, *, fec=False):
@overload
def decode(self, data: bytes, *, fec: bool) -> bytes:
...
@overload
def decode(self, data: Literal[None], *, fec: Literal[False]) -> bytes:
...
def decode(self, data: Optional[bytes], *, fec: bool = False) -> bytes:
if data is None and fec:
raise OpusError("Invalid arguments: FEC cannot be used with null data")
raise InvalidArgument("Invalid arguments: FEC cannot be used with null data")
if data is None:
frame_size = self._get_last_packet_duration() or self.SAMPLES_PER_FRAME
@ -418,4 +466,4 @@ class Decoder(_OpusStruct):
ret = _lib.opus_decode(self._state, data, len(data) if data else 0, pcm_ptr, frame_size, fec)
return array.array('h', pcm[:ret * channel_count]).tobytes()
return array.array("h", pcm[: ret * channel_count]).tobytes()

View File

@ -22,17 +22,36 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from .asset import Asset
from __future__ import annotations
from typing import Any, Dict, Optional, TYPE_CHECKING, Type, TypeVar, Union
import re
from .asset import Asset, AssetMixin
from .errors import InvalidArgument
from . import utils
__all__ = (
'PartialEmoji',
)
__all__ = ("PartialEmoji",)
if TYPE_CHECKING:
from .state import ConnectionState
from datetime import datetime
from .types.message import PartialEmoji as PartialEmojiPayload
class _EmojiTag:
__slots__ = ()
class PartialEmoji(_EmojiTag):
id: int
def _to_partial(self) -> PartialEmoji:
raise NotImplementedError
PE = TypeVar("PE", bound="PartialEmoji")
class PartialEmoji(_EmojiTag, AssetMixin):
"""Represents a "partial" emoji.
This model will be given in two scenarios:
@ -70,47 +89,92 @@ class PartialEmoji(_EmojiTag):
The ID of the custom emoji, if applicable.
"""
__slots__ = ('animated', 'name', 'id', '_state')
__slots__ = ("animated", "name", "id", "_state")
def __init__(self, *, name, animated=False, id=None):
_CUSTOM_EMOJI_RE = re.compile(r"<?(?P<animated>a)?:?(?P<name>[A-Za-z0-9\_]+):(?P<id>[0-9]{13,20})>?")
if TYPE_CHECKING:
id: Optional[int]
def __init__(self, *, name: str, animated: bool = False, id: Optional[int] = None):
self.animated = animated
self.name = name
self.id = id
self._state = None
self._state: Optional[ConnectionState] = None
@classmethod
def from_dict(cls, data):
def from_dict(cls: Type[PE], data: Union[PartialEmojiPayload, Dict[str, Any]]) -> PE:
return cls(
animated=data.get('animated', False),
id=utils._get_as_snowflake(data, 'id'),
name=data.get('name'),
animated=data.get("animated", False),
id=utils._get_as_snowflake(data, "id"),
name=data.get("name") or "",
)
def to_dict(self):
o = { 'name': self.name }
@classmethod
def from_str(cls: Type[PE], value: str) -> PE:
"""Converts a Discord string representation of an emoji to a :class:`PartialEmoji`.
The formats accepted are:
- ``a:name:id``
- ``<a:name:id>``
- ``name:id``
- ``<:name:id>``
If the format does not match then it is assumed to be a unicode emoji.
.. versionadded:: 2.0
Parameters
------------
value: :class:`str`
The string representation of an emoji.
Returns
--------
:class:`PartialEmoji`
The partial emoji from this string.
"""
match = cls._CUSTOM_EMOJI_RE.match(value)
if match is not None:
groups = match.groupdict()
animated = bool(groups["animated"])
emoji_id = int(groups["id"])
name = groups["name"]
return cls(name=name, animated=animated, id=emoji_id)
return cls(name=value, id=None, animated=False)
def to_dict(self) -> Dict[str, Any]:
o: Dict[str, Any] = {"name": self.name}
if self.id:
o['id'] = self.id
o["id"] = self.id
if self.animated:
o['animated'] = self.animated
o["animated"] = self.animated
return o
def _to_partial(self) -> PartialEmoji:
return self
@classmethod
def with_state(cls, state, *, name, animated=False, id=None):
def with_state(
cls: Type[PE], state: ConnectionState, *, name: str, animated: bool = False, id: Optional[int] = None
) -> PE:
self = cls(name=name, animated=animated, id=id)
self._state = state
return self
def __str__(self):
def __str__(self) -> str:
if self.id is None:
return self.name
if self.animated:
return f'<a:{self.name}:{self.id}>'
return f'<:{self.name}:{self.id}>'
return f"<a:{self.name}:{self.id}>"
return f"<:{self.name}:{self.id}>"
def __repr__(self):
return '<{0.__class__.__name__} animated={0.animated} name={0.name!r} id={0.id}>'.format(self)
return f"<{self.__class__.__name__} animated={self.animated} name={self.name!r} id={self.id}>"
def __eq__(self, other):
def __eq__(self, other: Any) -> bool:
if self.is_unicode_emoji():
return isinstance(other, PartialEmoji) and self.name == other.name
@ -118,75 +182,50 @@ class PartialEmoji(_EmojiTag):
return self.id == other.id
return False
def __ne__(self, other):
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __hash__(self):
def __hash__(self) -> int:
return hash((self.id, self.name))
def is_custom_emoji(self):
def is_custom_emoji(self) -> bool:
""":class:`bool`: Checks if this is a custom non-Unicode emoji."""
return self.id is not None
def is_unicode_emoji(self):
def is_unicode_emoji(self) -> bool:
""":class:`bool`: Checks if this is a Unicode emoji."""
return self.id is None
def _as_reaction(self):
def _as_reaction(self) -> str:
if self.id is None:
return self.name
return f'{self.name}:{self.id}'
return f"{self.name}:{self.id}"
@property
def created_at(self):
def created_at(self) -> Optional[datetime]:
"""Optional[:class:`datetime.datetime`]: Returns the emoji's creation time in UTC, or None if Unicode emoji.
.. versionadded:: 1.6
"""
if self.is_unicode_emoji():
if self.id is None:
return None
return utils.snowflake_time(self.id)
@property
def url(self):
""":class:`Asset`: Returns the asset of the emoji, if it is custom.
def url(self) -> str:
""":class:`str`: Returns the URL of the emoji, if it is custom.
This is equivalent to calling :meth:`url_as` with
the default parameters (i.e. png/gif detection).
"""
return self.url_as(format=None)
def url_as(self, *, format=None, static_format="png"):
"""Returns an :class:`Asset` for the emoji's url, if it is custom.
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif'.
'gif' is only valid for animated emojis.
.. versionadded:: 1.7
Parameters
-----------
format: Optional[:class:`str`]
The format to attempt to convert the emojis to.
If the format is ``None``, then it is automatically
detected as either 'gif' or static_format, depending on whether the
emoji is animated or not.
static_format: Optional[:class:`str`]
Format to attempt to convert only non-animated emoji's to.
Defaults to 'png'
Raises
-------
InvalidArgument
Bad image format passed to ``format`` or ``static_format``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
If this isn't a custom emoji then an empty string is returned
"""
if self.is_unicode_emoji():
return Asset(self._state)
return ""
return Asset._from_emoji(self._state, self, format=format, static_format=static_format)
fmt = "gif" if self.animated else "png"
return f"{Asset.BASE}/emojis/{self.id}.{fmt}"
async def read(self) -> bytes:
if self.is_unicode_emoji():
raise InvalidArgument("PartialEmoji is not a custom emoji")
return await super().read()

View File

@ -22,25 +22,34 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Callable, Any, ClassVar, Dict, Iterator, Set, TYPE_CHECKING, Tuple, Type, TypeVar, Optional
from .flags import BaseFlags, flag_value, fill_with_flags, alias_flag_value
__all__ = (
'Permissions',
'PermissionOverwrite',
"Permissions",
"PermissionOverwrite",
)
# A permission alias works like a regular flag but is marked
# So the PermissionOverwrite knows to work with it
class permission_alias(alias_flag_value):
pass
alias: str
def make_permission_alias(alias):
def decorator(func):
def make_permission_alias(alias: str) -> Callable[[Callable[[Any], int]], permission_alias]:
def decorator(func: Callable[[Any], int]) -> permission_alias:
ret = permission_alias(func)
ret.alias = alias
return ret
return decorator
P = TypeVar("P", bound="Permissions")
@fill_with_flags()
class Permissions(BaseFlags):
"""Wraps up the Discord permission value.
@ -92,35 +101,35 @@ class Permissions(BaseFlags):
__slots__ = ()
def __init__(self, permissions=0, **kwargs):
def __init__(self, permissions: int = 0, **kwargs: bool):
if not isinstance(permissions, int):
raise TypeError(f'Expected int parameter, received {permissions.__class__.__name__} instead.')
raise TypeError(f"Expected int parameter, received {permissions.__class__.__name__} instead.")
self.value = permissions
for key, value in kwargs.items():
if key not in self.VALID_FLAGS:
raise TypeError(f'{key!r} is not a valid permission name.')
raise TypeError(f"{key!r} is not a valid permission name.")
setattr(self, key, value)
def is_subset(self, other):
def is_subset(self, other: Permissions) -> bool:
"""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(f"cannot compare {self.__class__.__name__} with {other.__class__.__name__}")
def is_superset(self, other):
def is_superset(self, other: Permissions) -> bool:
"""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(f"cannot compare {self.__class__.__name__} with {other.__class__.__name__}")
def is_strict_subset(self, other):
def is_strict_subset(self, other: Permissions) -> bool:
"""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):
def is_strict_superset(self, other: Permissions) -> bool:
"""Returns ``True`` if the permissions on other are a strict superset of those on self."""
return self.is_superset(other) and self != other
@ -130,20 +139,20 @@ class Permissions(BaseFlags):
__gt__ = is_strict_superset
@classmethod
def none(cls):
def none(cls: Type[P]) -> P:
"""A factory method that creates a :class:`Permissions` with all
permissions set to ``False``."""
return cls(0)
@classmethod
def all(cls):
def all(cls: Type[P]) -> P:
"""A factory method that creates a :class:`Permissions` with all
permissions set to ``True``.
"""
return cls(0b111111111111111111111111111111111)
return cls(0b111111111111111111111111111111111111111)
@classmethod
def all_channel(cls):
def all_channel(cls: Type[P]) -> P:
"""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:
@ -160,11 +169,16 @@ class Permissions(BaseFlags):
.. versionchanged:: 1.7
Added :attr:`stream`, :attr:`priority_speaker` and :attr:`use_slash_commands` permissions.
.. versionchanged:: 2.0
Added :attr:`create_public_threads`, :attr:`create_private_threads`, :attr:`manage_threads`,
:attr:`use_external_stickers`, :attr:`send_messages_in_threads` and
:attr:`request_to_speak` permissions.
"""
return cls(0b10110011111101111111111101010001)
return cls(0b111110110110011111101111111111101010001)
@classmethod
def general(cls):
def general(cls: Type[P]) -> P:
"""A factory method that creates a :class:`Permissions` with all
"General" permissions from the official Discord UI set to ``True``.
@ -177,7 +191,7 @@ class Permissions(BaseFlags):
return cls(0b01110000000010000000010010110000)
@classmethod
def membership(cls):
def membership(cls: Type[P]) -> P:
"""A factory method that creates a :class:`Permissions` with all
"Membership" permissions from the official Discord UI set to ``True``.
@ -186,24 +200,28 @@ class Permissions(BaseFlags):
return cls(0b00001100000000000000000000000111)
@classmethod
def text(cls):
def text(cls: Type[P]) -> P:
"""A factory method that creates a :class:`Permissions` with all
"Text" permissions from the official Discord UI set to ``True``.
.. versionchanged:: 1.7
Permission :attr:`read_messages` is no longer part of the text permissions.
Added :attr:`use_slash_commands` permission.
.. versionchanged:: 2.0
Added :attr:`create_public_threads`, :attr:`create_private_threads`, :attr:`manage_threads`,
:attr:`send_messages_in_threads` and :attr:`use_external_stickers` permissions.
"""
return cls(0b10000000000001111111100001000000)
return cls(0b111110010000000000001111111100001000000)
@classmethod
def voice(cls):
def voice(cls: Type[P]) -> P:
"""A factory method that creates a :class:`Permissions` with all
"Voice" permissions from the official Discord UI set to ``True``."""
return cls(0b00000011111100000000001100000000)
@classmethod
def stage(cls):
def stage(cls: Type[P]) -> P:
"""A factory method that creates a :class:`Permissions` with all
"Stage Channel" permissions from the official Discord UI set to ``True``.
@ -212,7 +230,7 @@ class Permissions(BaseFlags):
return cls(1 << 32)
@classmethod
def stage_moderator(cls):
def stage_moderator(cls: Type[P]) -> P:
"""A factory method that creates a :class:`Permissions` with all
"Stage Moderator" permissions from the official Discord UI set to ``True``.
@ -221,7 +239,7 @@ class Permissions(BaseFlags):
return cls(0b100000001010000000000000000000000)
@classmethod
def advanced(cls):
def advanced(cls: Type[P]) -> P:
"""A factory method that creates a :class:`Permissions` with all
"Advanced" permissions from the official Discord UI set to ``True``.
@ -229,7 +247,7 @@ class Permissions(BaseFlags):
"""
return cls(1 << 3)
def update(self, **kwargs):
def update(self, **kwargs: bool) -> None:
r"""Bulk updates this permission object.
Allows you to set multiple attributes by using keyword
@ -245,7 +263,7 @@ class Permissions(BaseFlags):
if key in self.VALID_FLAGS:
setattr(self, key, value)
def handle_overwrite(self, allow, deny):
def handle_overwrite(self, allow: int, deny: int) -> None:
# 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
@ -261,67 +279,74 @@ class Permissions(BaseFlags):
self.value = (self.value & ~deny) | allow
@flag_value
def create_instant_invite(self):
def create_instant_invite(self) -> int:
""":class:`bool`: Returns ``True`` if the user can create instant invites."""
return 1 << 0
@flag_value
def kick_members(self):
def kick_members(self) -> int:
""":class:`bool`: Returns ``True`` if the user can kick users from the guild."""
return 1 << 1
@flag_value
def ban_members(self):
def ban_members(self) -> int:
""":class:`bool`: Returns ``True`` if a user can ban users from the guild."""
return 1 << 2
@flag_value
def administrator(self):
def administrator(self) -> int:
""":class:`bool`: Returns ``True`` if a user is an administrator. This role overrides all other permissions.
This also bypasses all channel-specific overrides.
"""
return 1 << 3
@make_permission_alias("administrator")
def admin(self) -> int:
""":class:`bool`: An alias for :attr:`administrator`.
.. versionadded:: 2.0
"""
return 1 << 3
@flag_value
def manage_channels(self):
def manage_channels(self) -> int:
""":class:`bool`: 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 1 << 4
@flag_value
def manage_guild(self):
def manage_guild(self) -> int:
""":class:`bool`: Returns ``True`` if a user can edit guild properties."""
return 1 << 5
@flag_value
def add_reactions(self):
def add_reactions(self) -> int:
""":class:`bool`: Returns ``True`` if a user can add reactions to messages."""
return 1 << 6
@flag_value
def view_audit_log(self):
def view_audit_log(self) -> int:
""":class:`bool`: Returns ``True`` if a user can view the guild's audit log."""
return 1 << 7
@flag_value
def priority_speaker(self):
def priority_speaker(self) -> int:
""":class:`bool`: Returns ``True`` if a user can be more easily heard while talking."""
return 1 << 8
@flag_value
def stream(self):
def stream(self) -> int:
""":class:`bool`: Returns ``True`` if a user can stream in a voice channel."""
return 1 << 9
@flag_value
def read_messages(self):
def read_messages(self) -> int:
""":class:`bool`: Returns ``True`` if a user can read messages from all or specific text channels."""
return 1 << 10
@make_permission_alias('read_messages')
def view_channel(self):
@make_permission_alias("read_messages")
def view_channel(self) -> int:
""":class:`bool`: An alias for :attr:`read_messages`.
.. versionadded:: 1.3
@ -329,17 +354,17 @@ class Permissions(BaseFlags):
return 1 << 10
@flag_value
def send_messages(self):
def send_messages(self) -> int:
""":class:`bool`: Returns ``True`` if a user can send messages from all or specific text channels."""
return 1 << 11
@flag_value
def send_tts_messages(self):
def send_tts_messages(self) -> int:
""":class:`bool`: Returns ``True`` if a user can send TTS messages from all or specific text channels."""
return 1 << 12
@flag_value
def manage_messages(self):
def manage_messages(self) -> int:
""":class:`bool`: Returns ``True`` if a user can delete or pin messages in a text channel.
.. note::
@ -349,32 +374,32 @@ class Permissions(BaseFlags):
return 1 << 13
@flag_value
def embed_links(self):
def embed_links(self) -> int:
""":class:`bool`: Returns ``True`` if a user's messages will automatically be embedded by Discord."""
return 1 << 14
@flag_value
def attach_files(self):
def attach_files(self) -> int:
""":class:`bool`: Returns ``True`` if a user can send files in their messages."""
return 1 << 15
@flag_value
def read_message_history(self):
def read_message_history(self) -> int:
""":class:`bool`: Returns ``True`` if a user can read a text channel's previous messages."""
return 1 << 16
@flag_value
def mention_everyone(self):
def mention_everyone(self) -> int:
""":class:`bool`: Returns ``True`` if a user's @everyone or @here will mention everyone in the text channel."""
return 1 << 17
@flag_value
def external_emojis(self):
def external_emojis(self) -> int:
""":class:`bool`: Returns ``True`` if a user can use emojis from other guilds."""
return 1 << 18
@make_permission_alias('external_emojis')
def use_external_emojis(self):
@make_permission_alias("external_emojis")
def use_external_emojis(self) -> int:
""":class:`bool`: An alias for :attr:`external_emojis`.
.. versionadded:: 1.3
@ -382,7 +407,7 @@ class Permissions(BaseFlags):
return 1 << 18
@flag_value
def view_guild_insights(self):
def view_guild_insights(self) -> int:
""":class:`bool`: Returns ``True`` if a user can view the guild's insights.
.. versionadded:: 1.3
@ -390,55 +415,55 @@ class Permissions(BaseFlags):
return 1 << 19
@flag_value
def connect(self):
def connect(self) -> int:
""":class:`bool`: Returns ``True`` if a user can connect to a voice channel."""
return 1 << 20
@flag_value
def speak(self):
def speak(self) -> int:
""":class:`bool`: Returns ``True`` if a user can speak in a voice channel."""
return 1 << 21
@flag_value
def mute_members(self):
def mute_members(self) -> int:
""":class:`bool`: Returns ``True`` if a user can mute other users."""
return 1 << 22
@flag_value
def deafen_members(self):
def deafen_members(self) -> int:
""":class:`bool`: Returns ``True`` if a user can deafen other users."""
return 1 << 23
@flag_value
def move_members(self):
def move_members(self) -> int:
""":class:`bool`: Returns ``True`` if a user can move users between other voice channels."""
return 1 << 24
@flag_value
def use_voice_activation(self):
def use_voice_activation(self) -> int:
""":class:`bool`: Returns ``True`` if a user can use voice activation in voice channels."""
return 1 << 25
@flag_value
def change_nickname(self):
def change_nickname(self) -> int:
""":class:`bool`: Returns ``True`` if a user can change their nickname in the guild."""
return 1 << 26
@flag_value
def manage_nicknames(self):
def manage_nicknames(self) -> int:
""":class:`bool`: Returns ``True`` if a user can change other user's nickname in the guild."""
return 1 << 27
@flag_value
def manage_roles(self):
def manage_roles(self) -> int:
""":class:`bool`: 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 1 << 28
@make_permission_alias('manage_roles')
def manage_permissions(self):
@make_permission_alias("manage_roles")
def manage_permissions(self) -> int:
""":class:`bool`: An alias for :attr:`manage_roles`.
.. versionadded:: 1.3
@ -446,17 +471,25 @@ class Permissions(BaseFlags):
return 1 << 28
@flag_value
def manage_webhooks(self):
def manage_webhooks(self) -> int:
""":class:`bool`: Returns ``True`` if a user can create, edit, or delete webhooks."""
return 1 << 29
@flag_value
def manage_emojis(self):
def manage_emojis(self) -> int:
""":class:`bool`: Returns ``True`` if a user can create, edit, or delete emojis."""
return 1 << 30
@make_permission_alias("manage_emojis")
def manage_emojis_and_stickers(self) -> int:
""":class:`bool`: An alias for :attr:`manage_emojis`.
.. versionadded:: 2.0
"""
return 1 << 30
@flag_value
def use_slash_commands(self):
def use_slash_commands(self) -> int:
""":class:`bool`: Returns ``True`` if a user can use slash commands.
.. versionadded:: 1.7
@ -464,14 +497,74 @@ class Permissions(BaseFlags):
return 1 << 31
@flag_value
def request_to_speak(self):
def request_to_speak(self) -> int:
""":class:`bool`: Returns ``True`` if a user can request to speak in a stage channel.
.. versionadded:: 1.7
"""
return 1 << 32
def augment_from_permissions(cls):
@flag_value
def manage_events(self) -> int:
""":class:`bool`: Returns ``True`` if a user can manage guild events.
.. versionadded:: 2.0
"""
return 1 << 33
@flag_value
def manage_threads(self) -> int:
""":class:`bool`: Returns ``True`` if a user can manage threads.
.. versionadded:: 2.0
"""
return 1 << 34
@flag_value
def create_public_threads(self) -> int:
""":class:`bool`: Returns ``True`` if a user can create public threads.
.. versionadded:: 2.0
"""
return 1 << 35
@flag_value
def create_private_threads(self) -> int:
""":class:`bool`: Returns ``True`` if a user can create private threads.
.. versionadded:: 2.0
"""
return 1 << 36
@flag_value
def external_stickers(self) -> int:
""":class:`bool`: Returns ``True`` if a user can use stickers from other guilds.
.. versionadded:: 2.0
"""
return 1 << 37
@make_permission_alias("external_stickers")
def use_external_stickers(self) -> int:
""":class:`bool`: An alias for :attr:`external_stickers`.
.. versionadded:: 2.0
"""
return 1 << 37
@flag_value
def send_messages_in_threads(self) -> int:
""":class:`bool`: Returns ``True`` if a user can send messages in threads.
.. versionadded:: 2.0
"""
return 1 << 38
PO = TypeVar("PO", bound="PermissionOverwrite")
def _augment_from_permissions(cls):
cls.VALID_NAMES = set(Permissions.VALID_FLAGS)
aliases = set()
@ -488,6 +581,7 @@ def augment_from_permissions(cls):
# god bless Python
def getter(self, x=key):
return self._values.get(x)
def setter(self, value, x=key):
self._set(x, value)
@ -497,7 +591,8 @@ def augment_from_permissions(cls):
cls.PURE_FLAGS = cls.VALID_NAMES - aliases
return cls
@augment_from_permissions
@_augment_from_permissions
class PermissionOverwrite:
r"""A type that is used to represent a channel specific permission.
@ -530,30 +625,79 @@ class PermissionOverwrite:
Set the value of permissions by their name.
"""
__slots__ = ('_values',)
__slots__ = ("_values",)
def __init__(self, **kwargs):
self._values = {}
if TYPE_CHECKING:
VALID_NAMES: ClassVar[Set[str]]
PURE_FLAGS: ClassVar[Set[str]]
# I wish I didn't have to do this
create_instant_invite: Optional[bool]
kick_members: Optional[bool]
ban_members: Optional[bool]
administrator: Optional[bool]
manage_channels: Optional[bool]
manage_guild: Optional[bool]
add_reactions: Optional[bool]
view_audit_log: Optional[bool]
priority_speaker: Optional[bool]
stream: Optional[bool]
read_messages: Optional[bool]
view_channel: Optional[bool]
send_messages: Optional[bool]
send_tts_messages: Optional[bool]
manage_messages: Optional[bool]
embed_links: Optional[bool]
attach_files: Optional[bool]
read_message_history: Optional[bool]
mention_everyone: Optional[bool]
external_emojis: Optional[bool]
use_external_emojis: Optional[bool]
view_guild_insights: Optional[bool]
connect: Optional[bool]
speak: Optional[bool]
mute_members: Optional[bool]
deafen_members: Optional[bool]
move_members: Optional[bool]
use_voice_activation: Optional[bool]
change_nickname: Optional[bool]
manage_nicknames: Optional[bool]
manage_roles: Optional[bool]
manage_permissions: Optional[bool]
manage_webhooks: Optional[bool]
manage_emojis: Optional[bool]
manage_emojis_and_stickers: Optional[bool]
use_slash_commands: Optional[bool]
request_to_speak: Optional[bool]
manage_events: Optional[bool]
manage_threads: Optional[bool]
create_public_threads: Optional[bool]
create_private_threads: Optional[bool]
send_messages_in_threads: Optional[bool]
external_stickers: Optional[bool]
use_external_stickers: Optional[bool]
def __init__(self, **kwargs: Optional[bool]):
self._values: Dict[str, Optional[bool]] = {}
for key, value in kwargs.items():
if key not in self.VALID_NAMES:
raise ValueError(f'no permission called {key}.')
raise ValueError(f"no permission called {key}.")
setattr(self, key, value)
def __eq__(self, other):
def __eq__(self, other: Any) -> bool:
return isinstance(other, PermissionOverwrite) and self._values == other._values
def _set(self, key, value):
def _set(self, key: str, value: Optional[bool]) -> None:
if value not in (True, None, False):
raise TypeError(f'Expected bool or NoneType, received {value.__class__.__name__}')
raise TypeError(f"Expected bool or NoneType, received {value.__class__.__name__}")
if value is None:
self._values.pop(key, None)
else:
self._values[key] = value
def pair(self):
def pair(self) -> Tuple[Permissions, Permissions]:
"""Tuple[:class:`Permissions`, :class:`Permissions`]: Returns the (allow, deny) pair from this overwrite."""
allow = Permissions.none()
@ -568,7 +712,7 @@ class PermissionOverwrite:
return allow, deny
@classmethod
def from_pair(cls, allow, deny):
def from_pair(cls: Type[PO], allow: Permissions, deny: Permissions) -> PO:
"""Creates an overwrite from an allow/deny pair of :class:`Permissions`."""
ret = cls()
for key, value in allow:
@ -581,7 +725,7 @@ class PermissionOverwrite:
return ret
def is_empty(self):
def is_empty(self) -> bool:
"""Checks if the permission overwrite is currently empty.
An empty permission overwrite is one that has no overwrites set
@ -594,7 +738,7 @@ class PermissionOverwrite:
"""
return len(self._values) == 0
def update(self, **kwargs):
def update(self, **kwargs: bool) -> None:
r"""Bulk updates this permission overwrite object.
Allows you to set multiple attributes by using keyword
@ -612,6 +756,6 @@ class PermissionOverwrite:
setattr(self, key, value)
def __iter__(self):
def __iter__(self) -> Iterator[Tuple[str, Optional[bool]]]:
for key in self.PURE_FLAGS:
yield key, self._values.get(key)

View File

@ -21,6 +21,7 @@ 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 __future__ import annotations
import threading
import traceback
@ -33,27 +34,41 @@ import time
import json
import sys
import re
import io
from typing import Any, Callable, Generic, IO, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union
from .errors import ClientException
from .opus import Encoder as OpusEncoder
from .oggparse import OggStream
from .utils import MISSING
log = logging.getLogger(__name__)
if TYPE_CHECKING:
from .voice_client import VoiceClient
AT = TypeVar("AT", bound="AudioSource")
FT = TypeVar("FT", bound="FFmpegOpusAudio")
_log = logging.getLogger(__name__)
__all__ = (
'AudioSource',
'PCMAudio',
'FFmpegAudio',
'FFmpegPCMAudio',
'FFmpegOpusAudio',
'PCMVolumeTransformer',
"AudioSource",
"PCMAudio",
"FFmpegAudio",
"FFmpegPCMAudio",
"FFmpegOpusAudio",
"PCMVolumeTransformer",
)
if sys.platform != 'win32':
CREATE_NO_WINDOW: int
if sys.platform != "win32":
CREATE_NO_WINDOW = 0
else:
CREATE_NO_WINDOW = 0x08000000
class AudioSource:
"""Represents an audio stream.
@ -65,7 +80,7 @@ class AudioSource:
The audio source reads are done in a separate thread.
"""
def read(self):
def read(self) -> bytes:
"""Reads 20ms worth of audio.
Subclasses must implement this.
@ -73,7 +88,7 @@ class AudioSource:
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
If :meth:`~AudioSource.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).
@ -85,11 +100,11 @@ class AudioSource:
"""
raise NotImplementedError
def is_opus(self):
def is_opus(self) -> bool:
"""Checks if the audio source is already encoded in Opus."""
return False
def cleanup(self):
def cleanup(self) -> None:
"""Called when clean-up is needed to be done.
Useful for clearing buffer data or processes after
@ -97,9 +112,10 @@ class AudioSource:
"""
pass
def __del__(self):
def __del__(self) -> None:
self.cleanup()
class PCMAudio(AudioSource):
"""Represents raw 16-bit 48KHz stereo PCM audio source.
@ -108,15 +124,17 @@ class PCMAudio(AudioSource):
stream: :term:`py:file object`
A file-like object that reads byte data representing raw PCM.
"""
def __init__(self, stream):
self.stream = stream
def read(self):
def __init__(self, stream: io.BufferedIOBase) -> None:
self.stream: io.BufferedIOBase = stream
def read(self) -> bytes:
ret = self.stream.read(OpusEncoder.FRAME_SIZE)
if len(ret) != OpusEncoder.FRAME_SIZE:
return b''
return b""
return ret
class FFmpegAudio(AudioSource):
"""Represents an FFmpeg (or AVConv) based AudioSource.
@ -126,48 +144,78 @@ class FFmpegAudio(AudioSource):
.. versionadded:: 1.3
"""
def __init__(self, source, *, executable='ffmpeg', args, **subprocess_kwargs):
self._process = self._stdout = None
def __init__(
self, source: Union[str, io.BufferedIOBase], *, executable: str = "ffmpeg", args: Any, **subprocess_kwargs: Any
):
piping = subprocess_kwargs.get("stdin") == subprocess.PIPE
if piping and isinstance(source, str):
raise TypeError("parameter conflict: 'source' parameter cannot be a string when piping to stdin")
args = [executable, *args]
kwargs = {'stdout': subprocess.PIPE}
kwargs = {"stdout": subprocess.PIPE}
kwargs.update(subprocess_kwargs)
self._process = self._spawn_process(args, **kwargs)
self._stdout = self._process.stdout
self._process: subprocess.Popen = self._spawn_process(args, **kwargs)
self._stdout: IO[bytes] = self._process.stdout # type: ignore
self._stdin: Optional[IO[Bytes]] = None
self._pipe_thread: Optional[threading.Thread] = None
def _spawn_process(self, args, **subprocess_kwargs):
if piping:
n = f"popen-stdin-writer:{id(self):#x}"
self._stdin = self._process.stdin
self._pipe_thread = threading.Thread(target=self._pipe_writer, args=(source,), daemon=True, name=n)
self._pipe_thread.start()
def _spawn_process(self, args: Any, **subprocess_kwargs: Any) -> subprocess.Popen:
process = None
try:
process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, **subprocess_kwargs)
except FileNotFoundError:
executable = args.partition(' ')[0] if isinstance(args, str) else args[0]
raise ClientException(executable + ' was not found.') from None
executable = args.partition(" ")[0] if isinstance(args, str) else args[0]
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
raise ClientException(f"Popen failed: {exc.__class__.__name__}: {exc}") from exc
else:
return process
def cleanup(self):
def _kill_process(self) -> None:
proc = self._process
if proc is None:
if proc is MISSING:
return
log.info('Preparing to terminate ffmpeg process %s.', proc.pid)
_log.info("Preparing to terminate ffmpeg process %s.", proc.pid)
try:
proc.kill()
except Exception:
log.exception("Ignoring error attempting to kill ffmpeg process %s", proc.pid)
_log.exception("Ignoring error attempting to kill ffmpeg process %s", proc.pid)
if proc.poll() is None:
log.info('ffmpeg process %s has not terminated. Waiting to terminate...', proc.pid)
_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)
_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)
_log.info("ffmpeg process %s successfully terminated with return code of %s.", proc.pid, proc.returncode)
def _pipe_writer(self, source: io.BufferedIOBase) -> None:
while self._process:
# arbitrarily large read size
data = source.read(8192)
if not data:
self._process.terminate()
return
try:
self._stdin.write(data)
except Exception:
_log.debug("Write error for %s, this is probably not a problem", self, exc_info=True)
# at this point the source data is either exhausted or the process is fubar
self._process.terminate()
return
def cleanup(self) -> None:
self._kill_process()
self._process = self._stdout = self._stdin = MISSING
self._process = self._stdout = None
class FFmpegPCMAudio(FFmpegAudio):
"""An audio source from FFmpeg (or AVConv).
@ -204,33 +252,43 @@ class FFmpegPCMAudio(FFmpegAudio):
The subprocess failed to be created.
"""
def __init__(self, source, *, executable='ffmpeg', pipe=False, stderr=None, before_options=None, options=None):
def __init__(
self,
source: Union[str, io.BufferedIOBase],
*,
executable: str = "ffmpeg",
pipe: bool = False,
stderr: Optional[IO[str]] = None,
before_options: Optional[str] = None,
options: Optional[str] = None,
) -> None:
args = []
subprocess_kwargs = {'stdin': source if pipe else subprocess.DEVNULL, 'stderr': stderr}
subprocess_kwargs = {"stdin": subprocess.PIPE if pipe else subprocess.DEVNULL, "stderr": stderr}
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'))
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')
args.append("pipe:1")
super().__init__(source, executable=executable, args=args, **subprocess_kwargs)
def read(self):
def read(self) -> bytes:
ret = self._stdout.read(OpusEncoder.FRAME_SIZE)
if len(ret) != OpusEncoder.FRAME_SIZE:
return b''
return b""
return ret
def is_opus(self):
def is_opus(self) -> bool:
return False
class FFmpegOpusAudio(FFmpegAudio):
"""An audio source from FFmpeg (or AVConv).
@ -292,38 +350,65 @@ class FFmpegOpusAudio(FFmpegAudio):
The subprocess failed to be created.
"""
def __init__(self, source, *, bitrate=128, codec=None, executable='ffmpeg',
pipe=False, stderr=None, before_options=None, options=None):
def __init__(
self,
source: Union[str, io.BufferedIOBase],
*,
bitrate: int = 128,
codec: Optional[str] = None,
executable: str = "ffmpeg",
pipe=False,
stderr=None,
before_options=None,
options=None,
) -> None:
args = []
subprocess_kwargs = {'stdin': source if pipe else subprocess.DEVNULL, 'stderr': stderr}
subprocess_kwargs = {"stdin": subprocess.PIPE if pipe else subprocess.DEVNULL, "stderr": stderr}
if isinstance(before_options, str):
args.extend(shlex.split(before_options))
args.append('-i')
args.append('-' if pipe else source)
args.append("-i")
args.append("-" if pipe else source)
codec = 'copy' if codec in ('opus', 'libopus') else 'libopus'
codec = "copy" if codec in ("opus", "libopus") else "libopus"
args.extend(('-map_metadata', '-1',
'-f', 'opus',
'-c:a', codec,
'-ar', '48000',
'-ac', '2',
'-b:a', f'{bitrate}k',
'-loglevel', 'warning'))
args.extend(
(
"-map_metadata",
"-1",
"-f",
"opus",
"-c:a",
codec,
"-ar",
"48000",
"-ac",
"2",
"-b:a",
f"{bitrate}k",
"-loglevel",
"warning",
)
)
if isinstance(options, str):
args.extend(shlex.split(options))
args.append('pipe:1')
args.append("pipe:1")
super().__init__(source, executable=executable, args=args, **subprocess_kwargs)
self._packet_iter = OggStream(self._stdout).iter_packets()
@classmethod
async def from_probe(cls, source, *, method=None, **kwargs):
async def from_probe(
cls: Type[FT],
source: str,
*,
method: Optional[Union[str, Callable[[str, str], Tuple[Optional[str], Optional[int]]]]] = None,
**kwargs: Any,
) -> FT:
"""|coro|
A factory method that creates a :class:`FFmpegOpusAudio` after probing
@ -347,7 +432,6 @@ class FFmpegOpusAudio(FFmpegAudio):
def custom_probe(source, executable):
# some analysis code here
return codec, bitrate
source = await discord.FFmpegOpusAudio.from_probe("song.webm", method=custom_probe)
@ -380,12 +464,18 @@ class FFmpegOpusAudio(FFmpegAudio):
An instance of this class.
"""
executable = kwargs.get('executable')
executable = kwargs.get("executable")
codec, bitrate = await cls.probe(source, method=method, executable=executable)
return cls(source, bitrate=bitrate, codec=codec, **kwargs)
return cls(source, bitrate=bitrate, codec=codec, **kwargs) # type: ignore
@classmethod
async def probe(cls, source, *, method=None, executable=None):
async def probe(
cls,
source: str,
*,
method: Optional[Union[str, Callable[[str, str], Tuple[Optional[str], Optional[int]]]]] = None,
executable: Optional[str] = None,
) -> Tuple[Optional[str], Optional[int]]:
"""|coro|
Probes the input source for bitrate and codec information.
@ -408,16 +498,16 @@ class FFmpegOpusAudio(FFmpegAudio):
Returns
---------
Tuple[Optional[:class:`str`], Optional[:class:`int`]]
Optional[Tuple[Optional[:class:`str`], Optional[:class:`int`]]]
A 2-tuple with the codec and bitrate of the input source.
"""
method = method or 'native'
executable = executable or 'ffmpeg'
method = method or "native"
executable = executable or "ffmpeg"
probefunc = fallback = None
if isinstance(method, str):
probefunc = getattr(cls, '_probe_codec_' + method, None)
probefunc = getattr(cls, "_probe_codec_" + method, None)
if probefunc is None:
raise AttributeError(f"Invalid probe method {method!r}")
@ -428,53 +518,52 @@ class FFmpegOpusAudio(FFmpegAudio):
probefunc = method
fallback = cls._probe_codec_fallback
else:
raise TypeError("Expected str or callable for parameter 'probe', " \
f"not '{method.__class__.__name__}'")
raise TypeError("Expected str or callable for parameter 'probe', " f"not '{method.__class__.__name__}'")
codec = bitrate = None
loop = asyncio.get_event_loop()
try:
codec, bitrate = await loop.run_in_executor(None, lambda: probefunc(source, executable))
codec, bitrate = await loop.run_in_executor(None, lambda: probefunc(source, executable)) # type: ignore
except Exception:
if not fallback:
log.exception("Probe '%s' using '%s' failed", method, executable)
return
_log.exception("Probe '%s' using '%s' failed", method, executable)
return # type: ignore
log.exception("Probe '%s' using '%s' failed, trying fallback", method, executable)
_log.exception("Probe '%s' using '%s' failed, trying fallback", method, executable)
try:
codec, bitrate = await loop.run_in_executor(None, lambda: fallback(source, executable))
codec, bitrate = await loop.run_in_executor(None, lambda: fallback(source, executable)) # type: ignore
except Exception:
log.exception("Fallback probe using '%s' failed", executable)
_log.exception("Fallback probe using '%s' failed", executable)
else:
log.info("Fallback probe found codec=%s, bitrate=%s", codec, bitrate)
_log.info("Fallback probe found codec=%s, bitrate=%s", codec, bitrate)
else:
log.info("Probe found codec=%s, bitrate=%s", codec, bitrate)
_log.info("Probe found codec=%s, bitrate=%s", codec, bitrate)
finally:
return codec, bitrate
@staticmethod
def _probe_codec_native(source, executable='ffmpeg'):
exe = executable[:2] + 'probe' if executable in ('ffmpeg', 'avconv') else executable
args = [exe, '-v', 'quiet', '-print_format', 'json', '-show_streams', '-select_streams', 'a:0', source]
def _probe_codec_native(source, executable: str = "ffmpeg") -> Tuple[Optional[str], Optional[int]]:
exe = executable[:2] + "probe" if executable in ("ffmpeg", "avconv") else executable
args = [exe, "-v", "quiet", "-print_format", "json", "-show_streams", "-select_streams", "a:0", source]
output = subprocess.check_output(args, timeout=20)
codec = bitrate = None
if output:
data = json.loads(output)
streamdata = data['streams'][0]
streamdata = data["streams"][0]
codec = streamdata.get('codec_name')
bitrate = int(streamdata.get('bit_rate', 0))
bitrate = max(round(bitrate/1000, 0), 512)
codec = streamdata.get("codec_name")
bitrate = int(streamdata.get("bit_rate", 0))
bitrate = max(round(bitrate / 1000), 512)
return codec, bitrate
@staticmethod
def _probe_codec_fallback(source, executable='ffmpeg'):
args = [executable, '-hide_banner', '-i', source]
def _probe_codec_fallback(source, executable: str = "ffmpeg") -> Tuple[Optional[str], Optional[int]]:
args = [executable, "-hide_banner", "-i", source]
proc = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
out, _ = proc.communicate(timeout=20)
output = out.decode('utf8')
output = out.decode("utf8")
codec = bitrate = None
codec_match = re.search(r"Stream #0.*?Audio: (\w+)", output)
@ -487,13 +576,14 @@ class FFmpegOpusAudio(FFmpegAudio):
return codec, bitrate
def read(self):
return next(self._packet_iter, b'')
def read(self) -> bytes:
return next(self._packet_iter, b"")
def is_opus(self):
def is_opus(self) -> bool:
return True
class PCMVolumeTransformer(AudioSource):
class PCMVolumeTransformer(AudioSource, Generic[AT]):
"""Transforms a previous :class:`AudioSource` to have volume controls.
This does not work on audio sources that have :meth:`AudioSource.is_opus`
@ -515,53 +605,54 @@ class PCMVolumeTransformer(AudioSource):
The audio source is opus encoded.
"""
def __init__(self, original, volume=1.0):
def __init__(self, original: AT, volume: float = 1.0):
if not isinstance(original, AudioSource):
raise TypeError(f'expected AudioSource not {original.__class__.__name__}.')
raise TypeError(f"expected AudioSource not {original.__class__.__name__}.")
if original.is_opus():
raise ClientException('AudioSource must not be Opus encoded.')
raise ClientException("AudioSource must not be Opus encoded.")
self.original = original
self.original: AT = original
self.volume = volume
@property
def volume(self):
def volume(self) -> float:
"""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):
def volume(self, value: float) -> None:
self._volume = max(value, 0.0)
def cleanup(self):
def cleanup(self) -> None:
self.original.cleanup()
def read(self):
def read(self) -> bytes:
ret = self.original.read()
return audioop.mul(ret, 2, min(self._volume, 2.0))
class AudioPlayer(threading.Thread):
DELAY = OpusEncoder.FRAME_LENGTH / 1000.0
DELAY: float = OpusEncoder.FRAME_LENGTH / 1000.0
def __init__(self, source, client, *, after=None):
def __init__(self, source: AudioSource, client: VoiceClient, *, after=None):
threading.Thread.__init__(self)
self.daemon = True
self.source = source
self.client = client
self.after = after
self.daemon: bool = True
self.source: AudioSource = source
self.client: VoiceClient = client
self.after: Optional[Callable[[Optional[Exception]], Any]] = 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()
self._end: threading.Event = threading.Event()
self._resumed: threading.Event = threading.Event()
self._resumed.set() # we are not paused
self._current_error: Optional[Exception] = None
self._connected: threading.Event = client._connected
self._lock: threading.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):
def _do_run(self) -> None:
self.loops = 0
self._start = time.perf_counter()
@ -596,7 +687,7 @@ class AudioPlayer(threading.Thread):
delay = max(0, self.DELAY + (next_time - time.perf_counter()))
time.sleep(delay)
def run(self):
def run(self) -> None:
try:
self._do_run()
except Exception as exc:
@ -606,53 +697,53 @@ class AudioPlayer(threading.Thread):
self.source.cleanup()
self._call_after()
def _call_after(self):
def _call_after(self) -> None:
error = self._current_error
if self.after is not None:
try:
self.after(error)
except Exception as exc:
log.exception('Calling the after function failed.')
_log.exception("Calling the after function failed.")
exc.__context__ = error
traceback.print_exception(type(exc), exc, exc.__traceback__)
elif error:
msg = f'Exception in voice thread {self.name}'
log.exception(msg, exc_info=error)
msg = f"Exception in voice thread {self.name}"
_log.exception(msg, exc_info=error)
print(msg, file=sys.stderr)
traceback.print_exception(type(error), error, error.__traceback__)
def stop(self):
def stop(self) -> None:
self._end.set()
self._resumed.set()
self._speak(False)
def pause(self, *, update_speaking=True):
def pause(self, *, update_speaking: bool = True) -> None:
self._resumed.clear()
if update_speaking:
self._speak(False)
def resume(self, *, update_speaking=True):
def resume(self, *, update_speaking: bool = True) -> None:
self.loops = 0
self._start = time.perf_counter()
self._resumed.set()
if update_speaking:
self._speak(True)
def is_playing(self):
def is_playing(self) -> bool:
return self._resumed.is_set() and not self._end.is_set()
def is_paused(self):
def is_paused(self) -> bool:
return not self._end.is_set() and not self._resumed.is_set()
def _set_source(self, source):
def _set_source(self, source: AudioSource) -> None:
with self._lock:
self.pause(update_speaking=False)
self.source = source
self.resume(update_speaking=False)
def _speak(self, speaking):
def _speak(self, speaking: bool) -> None:
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)
_log.info("Speaking call in player failed: %s", e)

0
discord/py.typed Normal file
View File

View File

@ -22,19 +22,44 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Optional, Set, List
if TYPE_CHECKING:
from .types.raw_models import (
MessageDeleteEvent,
BulkMessageDeleteEvent,
ReactionActionEvent,
MessageUpdateEvent,
ReactionClearEvent,
ReactionClearEmojiEvent,
IntegrationDeleteEvent,
TypingEvent,
)
from .message import Message
from .partial_emoji import PartialEmoji
from .member import Member
__all__ = (
'RawMessageDeleteEvent',
'RawBulkMessageDeleteEvent',
'RawMessageUpdateEvent',
'RawReactionActionEvent',
'RawReactionClearEvent',
'RawReactionClearEmojiEvent',
"RawMessageDeleteEvent",
"RawBulkMessageDeleteEvent",
"RawMessageUpdateEvent",
"RawReactionActionEvent",
"RawReactionClearEvent",
"RawReactionClearEmojiEvent",
"RawIntegrationDeleteEvent",
"RawTypingEvent",
)
class _RawReprMixin:
def __repr__(self):
value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__)
return f'<{self.__class__.__name__} {value}>'
def __repr__(self) -> str:
value = " ".join(f"{attr}={getattr(self, attr)!r}" for attr in self.__slots__)
return f"<{self.__class__.__name__} {value}>"
class RawMessageDeleteEvent(_RawReprMixin):
"""Represents the event payload for a :func:`on_raw_message_delete` event.
@ -51,16 +76,17 @@ class RawMessageDeleteEvent(_RawReprMixin):
The cached message, if found in the internal message cache.
"""
__slots__ = ('message_id', 'channel_id', 'guild_id', 'cached_message')
__slots__ = ("message_id", "channel_id", "guild_id", "cached_message")
def __init__(self, data):
self.message_id = int(data['id'])
self.channel_id = int(data['channel_id'])
self.cached_message = None
def __init__(self, data: MessageDeleteEvent) -> None:
self.message_id: int = int(data["id"])
self.channel_id: int = int(data["channel_id"])
self.cached_message: Optional[Message] = None
try:
self.guild_id = int(data['guild_id'])
self.guild_id: Optional[int] = int(data["guild_id"])
except KeyError:
self.guild_id = None
self.guild_id: Optional[int] = None
class RawBulkMessageDeleteEvent(_RawReprMixin):
"""Represents the event payload for a :func:`on_raw_bulk_message_delete` event.
@ -77,17 +103,18 @@ class RawBulkMessageDeleteEvent(_RawReprMixin):
The cached messages, if found in the internal message cache.
"""
__slots__ = ('message_ids', 'channel_id', 'guild_id', 'cached_messages')
__slots__ = ("message_ids", "channel_id", "guild_id", "cached_messages")
def __init__(self, data):
self.message_ids = {int(x) for x in data.get('ids', [])}
self.channel_id = int(data['channel_id'])
self.cached_messages = []
def __init__(self, data: BulkMessageDeleteEvent) -> None:
self.message_ids: Set[int] = {int(x) for x in data.get("ids", [])}
self.channel_id: int = int(data["channel_id"])
self.cached_messages: List[Message] = []
try:
self.guild_id = int(data['guild_id'])
self.guild_id: Optional[int] = int(data["guild_id"])
except KeyError:
self.guild_id = None
self.guild_id: Optional[int] = None
class RawMessageUpdateEvent(_RawReprMixin):
"""Represents the payload for a :func:`on_raw_message_edit` event.
@ -112,18 +139,19 @@ class RawMessageUpdateEvent(_RawReprMixin):
it is modified by the data in :attr:`RawMessageUpdateEvent.data`.
"""
__slots__ = ('message_id', 'channel_id', 'guild_id', 'data', 'cached_message')
__slots__ = ("message_id", "channel_id", "guild_id", "data", "cached_message")
def __init__(self, data):
self.message_id = int(data['id'])
self.channel_id = int(data['channel_id'])
self.data = data
self.cached_message = None
def __init__(self, data: MessageUpdateEvent) -> None:
self.message_id: int = int(data["id"])
self.channel_id: int = int(data["channel_id"])
self.data: MessageUpdateEvent = data
self.cached_message: Optional[Message] = None
try:
self.guild_id = int(data['guild_id'])
self.guild_id: Optional[int] = int(data["guild_id"])
except KeyError:
self.guild_id = None
self.guild_id: Optional[int] = None
class RawReactionActionEvent(_RawReprMixin):
"""Represents the payload for a :func:`on_raw_reaction_add` or
@ -154,21 +182,21 @@ class RawReactionActionEvent(_RawReprMixin):
.. versionadded:: 1.3
"""
__slots__ = ('message_id', 'user_id', 'channel_id', 'guild_id', 'emoji',
'event_type', 'member')
__slots__ = ("message_id", "user_id", "channel_id", "guild_id", "emoji", "event_type", "member")
def __init__(self, data, emoji, event_type):
self.message_id = int(data['message_id'])
self.channel_id = int(data['channel_id'])
self.user_id = int(data['user_id'])
self.emoji = emoji
self.event_type = event_type
self.member = None
def __init__(self, data: ReactionActionEvent, emoji: PartialEmoji, event_type: str) -> None:
self.message_id: int = int(data["message_id"])
self.channel_id: int = int(data["channel_id"])
self.user_id: int = int(data["user_id"])
self.emoji: PartialEmoji = emoji
self.event_type: str = event_type
self.member: Optional[Member] = None
try:
self.guild_id = int(data['guild_id'])
self.guild_id: Optional[int] = int(data["guild_id"])
except KeyError:
self.guild_id = None
self.guild_id: Optional[int] = None
class RawReactionClearEvent(_RawReprMixin):
"""Represents the payload for a :func:`on_raw_reaction_clear` event.
@ -183,16 +211,17 @@ class RawReactionClearEvent(_RawReprMixin):
The guild ID where the reactions got cleared.
"""
__slots__ = ('message_id', 'channel_id', 'guild_id')
__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'])
def __init__(self, data: ReactionClearEvent) -> None:
self.message_id: int = int(data["message_id"])
self.channel_id: int = int(data["channel_id"])
try:
self.guild_id = int(data['guild_id'])
self.guild_id: Optional[int] = int(data["guild_id"])
except KeyError:
self.guild_id = None
self.guild_id: Optional[int] = None
class RawReactionClearEmojiEvent(_RawReprMixin):
"""Represents the payload for a :func:`on_raw_reaction_clear_emoji` event.
@ -211,14 +240,74 @@ class RawReactionClearEmojiEvent(_RawReprMixin):
The custom or unicode emoji being removed.
"""
__slots__ = ('message_id', 'channel_id', 'guild_id', 'emoji')
__slots__ = ("message_id", "channel_id", "guild_id", "emoji")
def __init__(self, data, emoji):
self.emoji = emoji
self.message_id = int(data['message_id'])
self.channel_id = int(data['channel_id'])
def __init__(self, data: ReactionClearEmojiEvent, emoji: PartialEmoji) -> None:
self.emoji: PartialEmoji = emoji
self.message_id: int = int(data["message_id"])
self.channel_id: int = int(data["channel_id"])
try:
self.guild_id = int(data['guild_id'])
self.guild_id: Optional[int] = int(data["guild_id"])
except KeyError:
self.guild_id = None
self.guild_id: Optional[int] = None
class RawIntegrationDeleteEvent(_RawReprMixin):
"""Represents the payload for a :func:`on_raw_integration_delete` event.
.. versionadded:: 2.0
Attributes
-----------
integration_id: :class:`int`
The ID of the integration that got deleted.
application_id: Optional[:class:`int`]
The ID of the bot/OAuth2 application for this deleted integration.
guild_id: :class:`int`
The guild ID where the integration got deleted.
"""
__slots__ = ("integration_id", "application_id", "guild_id")
def __init__(self, data: IntegrationDeleteEvent) -> None:
self.integration_id: int = int(data["id"])
self.guild_id: int = int(data["guild_id"])
try:
self.application_id: Optional[int] = int(data["application_id"])
except KeyError:
self.application_id: Optional[int] = None
class RawTypingEvent(_RawReprMixin):
"""Represents the payload for a :func:`on_raw_typing` event.
.. versionadded:: 2.0
Attributes
-----------
channel_id: :class:`int`
The channel ID where the typing originated from.
user_id: :class:`int`
The ID of the user that started typing.
when: :class:`datetime.datetime`
When the typing started as an aware datetime in UTC.
guild_id: Optional[:class:`int`]
The guild ID where the typing originated from, if applicable.
member: Optional[:class:`Member`]
The member who started typing. Only available if the member started typing in a guild.
"""
__slots__ = ("channel_id", "user_id", "when", "guild_id", "member")
def __init__(self, data: TypingEvent) -> None:
self.channel_id: int = int(data["channel_id"])
self.user_id: int = int(data["user_id"])
self.when: datetime.datetime = datetime.datetime.fromtimestamp(data.get("timestamp"), tz=datetime.timezone.utc)
self.member: Optional[Member] = None
try:
self.guild_id: Optional[int] = int(data["guild_id"])
except KeyError:
self.guild_id: Optional[int] = None

View File

@ -22,11 +22,20 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Any, TYPE_CHECKING, Union, Optional
from .iterators import ReactionIterator
__all__ = (
'Reaction',
)
__all__ = ("Reaction",)
if TYPE_CHECKING:
from .types.message import Reaction as ReactionPayload
from .message import Message
from .partial_emoji import PartialEmoji
from .emoji import Emoji
from .abc import Snowflake
class Reaction:
"""Represents a reaction to a message.
@ -65,37 +74,40 @@ class 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')
__slots__ = ("message", "count", "emoji", "me")
@property
def custom_emoji(self):
def __init__(
self, *, message: Message, data: ReactionPayload, emoji: Optional[Union[PartialEmoji, Emoji, str]] = None
):
self.message: Message = message
self.emoji: Union[PartialEmoji, Emoji, str] = emoji or message._state.get_reaction_emoji(data["emoji"])
self.count: int = data.get("count", 1)
self.me: bool = data.get("me")
# TODO: typeguard
def is_custom_emoji(self) -> bool:
""":class:`bool`: If this is a custom emoji."""
return not isinstance(self.emoji, str)
def __eq__(self, other):
def __eq__(self, other: Any) -> bool:
return isinstance(other, self.__class__) and other.emoji == self.emoji
def __ne__(self, other):
def __ne__(self, other: Any) -> bool:
if isinstance(other, self.__class__):
return other.emoji != self.emoji
return True
def __hash__(self):
def __hash__(self) -> int:
return hash(self.emoji)
def __str__(self):
def __str__(self) -> str:
return str(self.emoji)
def __repr__(self):
return '<Reaction emoji={0.emoji!r} me={0.me} count={0.count}>'.format(self)
def __repr__(self) -> str:
return f"<Reaction emoji={self.emoji!r} me={self.me} count={self.count}>"
async def remove(self, user):
async def remove(self, user: Snowflake) -> None:
"""|coro|
Remove the reaction by the provided :class:`User` from the message.
@ -123,7 +135,7 @@ class Reaction:
await self.message.remove_reaction(self.emoji, user)
async def clear(self):
async def clear(self) -> None:
"""|coro|
Clears this reaction from the message.
@ -145,7 +157,7 @@ class Reaction:
"""
await self.message.clear_reaction(self.emoji)
def users(self, limit=None, after=None):
def users(self, *, limit: Optional[int] = None, after: Optional[Snowflake] = None) -> ReactionIterator:
"""Returns an :class:`AsyncIterator` representing the users that have reacted to the message.
The ``after`` parameter must represent a member
@ -158,22 +170,22 @@ class Reaction:
# 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))
await channel.send(f'{user} has reacted with {reaction.emoji}!')
Flattening into a list: ::
users = await reaction.users().flatten()
# users is now a list of User...
winner = random.choice(users)
await channel.send('{} has won the raffle.'.format(winner))
await channel.send(f'{winner} has won the raffle.')
Parameters
------------
limit: :class:`int`
limit: Optional[:class:`int`]
The maximum number of results to return.
If not provided, returns all the users who
reacted to the message.
after: :class:`abc.Snowflake`
after: Optional[:class:`abc.Snowflake`]
For pagination, reactions are sorted by member.
Raises
@ -190,8 +202,8 @@ class Reaction:
if the member has left the guild.
"""
if self.custom_emoji:
emoji = '{0.name}:{0.id}'.format(self.emoji)
if not isinstance(self.emoji, str):
emoji = f"{self.emoji.name}:{self.emoji.id}"
else:
emoji = self.emoji

View File

@ -22,17 +22,32 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, TypeVar, Union, overload, TYPE_CHECKING
from .permissions import Permissions
from .errors import InvalidArgument
from .colour import Colour
from .mixins import Hashable
from .utils import snowflake_time, _get_as_snowflake
from .utils import snowflake_time, _get_as_snowflake, MISSING
__all__ = (
'RoleTags',
'Role',
"RoleTags",
"Role",
)
if TYPE_CHECKING:
import datetime
from .types.role import (
Role as RolePayload,
RoleTags as RoleTagPayload,
)
from .types.guild import RolePositionUpdate
from .guild import Guild
from .member import Member
from .state import ConnectionState
class RoleTags:
"""Represents tags on a role.
@ -52,32 +67,42 @@ class RoleTags:
The integration ID that manages the role.
"""
__slots__ = ('bot_id', 'integration_id', '_premium_subscriber',)
__slots__ = (
"bot_id",
"integration_id",
"_premium_subscriber",
)
def __init__(self, data):
self.bot_id = _get_as_snowflake(data, 'bot_id')
self.integration_id = _get_as_snowflake(data, 'integration_id')
def __init__(self, data: RoleTagPayload):
self.bot_id: Optional[int] = _get_as_snowflake(data, "bot_id")
self.integration_id: Optional[int] = _get_as_snowflake(data, "integration_id")
# NOTE: The API returns "null" for this if it's valid, which corresponds to None.
# This is different from other fields where "null" means "not there".
# So in this case, a value of None is the same as True.
# Which means we would need a different sentinel. For this purpose I used ellipsis.
self._premium_subscriber = data.get('premium_subscriber', ...)
# Which means we would need a different sentinel.
self._premium_subscriber: Optional[Any] = data.get("premium_subscriber", MISSING)
def is_bot_managed(self):
def is_bot_managed(self) -> bool:
""":class:`bool`: Whether the role is associated with a bot."""
return self.bot_id is not None
def is_premium_subscriber(self):
def is_premium_subscriber(self) -> bool:
""":class:`bool`: Whether the role is the premium subscriber, AKA "boost", role for the guild."""
return self._premium_subscriber is None
def is_integration(self):
def is_integration(self) -> bool:
""":class:`bool`: Whether the role is managed by an integration."""
return self.integration_id is not None
def __repr__(self):
return '<RoleTags bot_id={0.bot_id} integration_id={0.integration_id} ' \
'premium_subscriber={1}>'.format(self, self.is_premium_subscriber())
def __repr__(self) -> str:
return (
f"<RoleTags bot_id={self.bot_id} integration_id={self.integration_id} "
f"premium_subscriber={self.is_premium_subscriber()}>"
)
R = TypeVar("R", bound="Role")
class Role(Hashable):
"""Represents a Discord role in a :class:`Guild`.
@ -116,6 +141,14 @@ class Role(Hashable):
Returns the role's name.
.. describe:: str(x)
Returns the role's ID.
.. describe:: int(x)
Returns the role's ID.
Attributes
----------
id: :class:`int`
@ -129,6 +162,15 @@ class Role(Hashable):
position: :class:`int`
The position of the role. This number is usually positive. The bottom
role has a position of 0.
.. warning::
Multiple roles can have the same position number. As a consequence
of this, comparing via role position is prone to subtle bugs if
checking for role hierarchy. The recommended and correct way to
compare for roles in the hierarchy is using the comparison
operators on the role objects themselves.
managed: :class:`bool`
Indicates if the role is managed by the guild through some form of
integrations such as Twitch.
@ -138,27 +180,41 @@ class Role(Hashable):
The role tags associated with this role.
"""
__slots__ = ('id', 'name', '_permissions', '_colour', 'position',
'managed', 'mentionable', 'hoist', 'guild', 'tags', '_state')
__slots__ = (
"id",
"name",
"_permissions",
"_colour",
"position",
"managed",
"mentionable",
"hoist",
"guild",
"tags",
"_state",
)
def __init__(self, *, guild, state, data):
self.guild = guild
self._state = state
self.id = int(data['id'])
def __init__(self, *, guild: Guild, state: ConnectionState, data: RolePayload):
self.guild: Guild = guild
self._state: ConnectionState = state
self.id: int = int(data["id"])
self._update(data)
def __str__(self):
def __str__(self) -> str:
return self.name
def __repr__(self):
return '<Role id={0.id} name={0.name!r}>'.format(self)
def __int__(self) -> int:
return self.id
def __lt__(self, other):
def __repr__(self) -> str:
return f"<Role id={self.id} name={self.name!r}>"
def __lt__(self: R, other: R) -> bool:
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.')
raise RuntimeError("cannot compare roles from two different guilds.")
# the @everyone role is always the lowest role in hierarchy
guild_id = self.guild.id
@ -174,87 +230,96 @@ class Role(Hashable):
return False
def __le__(self, other):
def __le__(self: R, other: R) -> bool:
r = Role.__lt__(other, self)
if r is NotImplemented:
return NotImplemented
return not r
def __gt__(self, other):
def __gt__(self: R, other: R) -> bool:
return Role.__lt__(other, self)
def __ge__(self, other):
def __ge__(self: R, other: R) -> bool:
r = Role.__lt__(self, other)
if r is NotImplemented:
return NotImplemented
return not r
def _update(self, data):
self.name = data['name']
self._permissions = int(data.get('permissions_new', 0))
self.position = data.get('position', 0)
self._colour = data.get('color', 0)
self.hoist = data.get('hoist', False)
self.managed = data.get('managed', False)
self.mentionable = data.get('mentionable', False)
def _update(self, data: RolePayload):
self.name: str = data["name"]
self._permissions: int = int(data.get("permissions", 0))
self.position: int = data.get("position", 0)
self._colour: int = data.get("color", 0)
self.hoist: bool = data.get("hoist", False)
self.managed: bool = data.get("managed", False)
self.mentionable: bool = data.get("mentionable", False)
self.tags: Optional[RoleTags]
try:
self.tags = RoleTags(data['tags'])
self.tags = RoleTags(data["tags"])
except KeyError:
self.tags = None
def is_default(self):
def is_default(self) -> bool:
""":class:`bool`: Checks if the role is the default role."""
return self.guild.id == self.id
def is_bot_managed(self):
def is_bot_managed(self) -> bool:
""":class:`bool`: Whether the role is associated with a bot.
.. versionadded:: 1.6
"""
return self.tags is not None and self.tags.is_bot_managed()
def is_premium_subscriber(self):
def is_premium_subscriber(self) -> bool:
""":class:`bool`: Whether the role is the premium subscriber, AKA "boost", role for the guild.
.. versionadded:: 1.6
"""
return self.tags is not None and self.tags.is_premium_subscriber()
def is_integration(self):
def is_integration(self) -> bool:
""":class:`bool`: Whether the role is managed by an integration.
.. versionadded:: 1.6
"""
return self.tags is not None and self.tags.is_integration()
def is_assignable(self) -> bool:
""":class:`bool`: Whether the role is able to be assigned or removed by the bot.
.. versionadded:: 2.0
"""
me = self.guild.me
return not self.is_default() and not self.managed and (me.top_role > self or me.id == self.guild.owner_id)
@property
def permissions(self):
def permissions(self) -> Permissions:
""":class:`Permissions`: Returns the role's permissions."""
return Permissions(self._permissions)
@property
def colour(self):
def colour(self) -> Colour:
""":class:`Colour`: Returns the role colour. An alias exists under ``color``."""
return Colour(self._colour)
@property
def color(self):
def color(self) -> Colour:
""":class:`Colour`: Returns the role color. An alias exists under ``colour``."""
return self.colour
@property
def created_at(self):
def created_at(self) -> datetime.datetime:
""":class:`datetime.datetime`: Returns the role's creation time in UTC."""
return snowflake_time(self.id)
@property
def mention(self):
def mention(self) -> str:
""":class:`str`: Returns a string that allows you to mention a role."""
return f'<@&{self.id}>'
return f"<@&{self.id}>"
@property
def members(self):
def members(self) -> List[Member]:
"""List[:class:`Member`]: Returns all the members with this role."""
all_members = self.guild.members
if self.is_default():
@ -263,7 +328,7 @@ class Role(Hashable):
role_id = self.id
return [member for member in all_members if member._roles.has(role_id)]
async def _move(self, position, reason):
async def _move(self, position: int, reason: Optional[str]) -> None:
if position <= 0:
raise InvalidArgument("Cannot move role to position 0 or below")
@ -283,10 +348,21 @@ class Role(Hashable):
else:
roles.append(self.id)
payload = [{"id": z[0], "position": z[1]} for z in zip(roles, change_range)]
payload: List[RolePositionUpdate] = [{"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):
async def edit(
self,
*,
name: str = MISSING,
permissions: Permissions = MISSING,
colour: Union[Colour, int] = MISSING,
color: Union[Colour, int] = MISSING,
hoist: bool = MISSING,
mentionable: bool = MISSING,
position: int = MISSING,
reason: Optional[str] = MISSING,
) -> Optional[Role]:
"""|coro|
Edits the role.
@ -299,6 +375,9 @@ class Role(Hashable):
.. versionchanged:: 1.4
Can now pass ``int`` to ``colour`` keyword-only parameter.
.. versionchanged:: 2.0
Edits are no longer in-place, the newly edited role is returned instead.
Parameters
-----------
name: :class:`str`
@ -326,33 +405,41 @@ class Role(Hashable):
InvalidArgument
An invalid position was given or the default
role was asked to be moved.
Returns
--------
:class:`Role`
The newly edited role.
"""
position = fields.get('position')
if position is not None:
if position is not MISSING:
await self._move(position, reason=reason)
self.position = position
try:
colour = fields['colour']
except KeyError:
colour = fields.get('color', self.colour)
payload: Dict[str, Any] = {}
if color is not MISSING:
colour = color
if isinstance(colour, int):
colour = Colour(value=colour)
if colour is not MISSING:
if isinstance(colour, int):
payload["color"] = colour
else:
payload["color"] = colour.value
payload = {
'name': fields.get('name', self.name),
'permissions': str(fields.get('permissions', self.permissions).value),
'color': colour.value,
'hoist': fields.get('hoist', self.hoist),
'mentionable': fields.get('mentionable', self.mentionable)
}
if name is not MISSING:
payload["name"] = name
if permissions is not MISSING:
payload["permissions"] = permissions.value
if hoist is not MISSING:
payload["hoist"] = hoist
if mentionable is not MISSING:
payload["mentionable"] = mentionable
data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload)
self._update(data)
return Role(guild=self.guild, data=data, state=self._state)
async def delete(self, *, reason=None):
async def delete(self, *, reason: Optional[str] = None) -> None:
"""|coro|
Deletes the role.

View File

@ -22,8 +22,9 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import asyncio
import itertools
import logging
import aiohttp
@ -34,22 +35,30 @@ from .backoff import ExponentialBackoff
from .gateway import *
from .errors import (
ClientException,
InvalidArgument,
HTTPException,
GatewayNotFound,
ConnectionClosed,
PrivilegedIntentsRequired,
)
from . import utils
from .enums import Status
from typing import TYPE_CHECKING, Any, Callable, Tuple, Type, Optional, List, Dict, TypeVar
if TYPE_CHECKING:
from .gateway import DiscordWebSocket
from .activity import BaseActivity
from .enums import Status
EI = TypeVar("EI", bound="EventItem")
__all__ = (
'AutoShardedClient',
'ShardInfo',
"AutoShardedClient",
"ShardInfo",
)
log = logging.getLogger(__name__)
_log = logging.getLogger(__name__)
class EventType:
close = 0
@ -59,39 +68,41 @@ class EventType:
terminate = 4
clean_close = 5
class EventItem:
__slots__ = ('type', 'shard', 'error')
__slots__ = ("type", "shard", "error")
def __init__(self, etype, shard, error):
self.type = etype
self.shard = shard
self.error = error
def __init__(self, etype: int, shard: Optional["Shard"], error: Optional[Exception]) -> None:
self.type: int = etype
self.shard: Optional["Shard"] = shard
self.error: Optional[Exception] = error
def __lt__(self, other):
def __lt__(self: EI, other: EI) -> bool:
if not isinstance(other, EventItem):
return NotImplemented
return self.type < other.type
def __eq__(self, other):
def __eq__(self: EI, other: EI) -> bool:
if not isinstance(other, EventItem):
return NotImplemented
return self.type == other.type
def __hash__(self):
def __hash__(self) -> int:
return hash(self.type)
class Shard:
def __init__(self, ws, client, queue_put):
self.ws = ws
self._client = client
self._dispatch = client.dispatch
self._queue_put = queue_put
self.loop = self._client.loop
self._disconnect = False
def __init__(self, ws: DiscordWebSocket, client: AutoShardedClient, queue_put: Callable[[EventItem], None]) -> None:
self.ws: DiscordWebSocket = ws
self._client: Client = client
self._dispatch: Callable[..., None] = client.dispatch
self._queue_put: Callable[[EventItem], None] = queue_put
self.loop: asyncio.AbstractEventLoop = self._client.loop
self._disconnect: bool = False
self._reconnect = client._reconnect
self._backoff = ExponentialBackoff()
self._task = None
self._handled_exceptions = (
self._backoff: ExponentialBackoff = ExponentialBackoff()
self._task: Optional[asyncio.Task] = None
self._handled_exceptions: Tuple[Type[Exception], ...] = (
OSError,
HTTPException,
GatewayNotFound,
@ -101,27 +112,28 @@ class Shard:
)
@property
def id(self):
return self.ws.shard_id
def id(self) -> int:
# DiscordWebSocket.shard_id is set in the from_client classmethod
return self.ws.shard_id # type: ignore
def launch(self):
def launch(self) -> None:
self._task = self.loop.create_task(self.worker())
def _cancel_task(self):
def _cancel_task(self) -> None:
if self._task is not None and not self._task.done():
self._task.cancel()
async def close(self):
async def close(self) -> None:
self._cancel_task()
await self.ws.close(code=1000)
async def disconnect(self):
async def disconnect(self) -> None:
await self.close()
self._dispatch('shard_disconnect', self.id)
self._dispatch("shard_disconnect", self.id)
async def _handle_disconnect(self, e):
self._dispatch('disconnect')
self._dispatch('shard_disconnect', self.id)
async def _handle_disconnect(self, e: Exception) -> None:
self._dispatch("disconnect")
self._dispatch("shard_disconnect", self.id)
if not self._reconnect:
self._queue_put(EventItem(EventType.close, self, e))
return
@ -144,11 +156,11 @@ class Shard:
return
retry = self._backoff.delay()
log.error('Attempting a reconnect for shard ID %s in %.2fs', self.id, retry, exc_info=e)
_log.error("Attempting a reconnect for shard ID %s in %.2fs", self.id, retry, exc_info=e)
await asyncio.sleep(retry)
self._queue_put(EventItem(EventType.reconnect, self, e))
async def worker(self):
async def worker(self) -> None:
while not self._client.is_closed():
try:
await self.ws.poll_event()
@ -165,14 +177,19 @@ class Shard:
self._queue_put(EventItem(EventType.terminate, self, e))
break
async def reidentify(self, exc):
async def reidentify(self, exc: ReconnectWebSocket) -> None:
self._cancel_task()
self._dispatch('disconnect')
self._dispatch('shard_disconnect', self.id)
log.info('Got a request to %s the websocket at Shard ID %s.', exc.op, self.id)
self._dispatch("disconnect")
self._dispatch("shard_disconnect", self.id)
_log.info("Got a request to %s the websocket at Shard ID %s.", exc.op, self.id)
try:
coro = DiscordWebSocket.from_client(self._client, resume=exc.resume, shard_id=self.id,
session=self.ws.session_id, sequence=self.ws.sequence)
coro = DiscordWebSocket.from_client(
self._client,
resume=exc.resume,
shard_id=self.id,
session=self.ws.session_id,
sequence=self.ws.sequence,
)
self.ws = await asyncio.wait_for(coro, timeout=60.0)
except self._handled_exceptions as e:
await self._handle_disconnect(e)
@ -183,7 +200,7 @@ class Shard:
else:
self.launch()
async def reconnect(self):
async def reconnect(self) -> None:
self._cancel_task()
try:
coro = DiscordWebSocket.from_client(self._client, shard_id=self.id)
@ -197,6 +214,7 @@ class Shard:
else:
self.launch()
class ShardInfo:
"""A class that gives information and control over a specific shard.
@ -213,18 +231,18 @@ class ShardInfo:
The shard count for this cluster. If this is ``None`` then the bot has not started yet.
"""
__slots__ = ('_parent', 'id', 'shard_count')
__slots__ = ("_parent", "id", "shard_count")
def __init__(self, parent, shard_count):
self._parent = parent
self.id = parent.id
self.shard_count = shard_count
def __init__(self, parent: Shard, shard_count: Optional[int]) -> None:
self._parent: Shard = parent
self.id: int = parent.id
self.shard_count: Optional[int] = shard_count
def is_closed(self):
def is_closed(self) -> bool:
""":class:`bool`: Whether the shard connection is currently closed."""
return not self._parent.ws.open
async def disconnect(self):
async def disconnect(self) -> None:
"""|coro|
Disconnects a shard. When this is called, the shard connection will no
@ -237,7 +255,7 @@ class ShardInfo:
await self._parent.disconnect()
async def reconnect(self):
async def reconnect(self) -> None:
"""|coro|
Disconnects and then connects the shard again.
@ -246,7 +264,7 @@ class ShardInfo:
await self._parent.disconnect()
await self._parent.reconnect()
async def connect(self):
async def connect(self) -> None:
"""|coro|
Connects a shard. If the shard is already connected this does nothing.
@ -257,11 +275,11 @@ class ShardInfo:
await self._parent.reconnect()
@property
def latency(self):
def latency(self) -> float:
""":class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds for this shard."""
return self._parent.ws.latency
def is_ws_ratelimited(self):
def is_ws_ratelimited(self) -> bool:
""":class:`bool`: Whether the websocket is currently rate limited.
This can be useful to know when deciding whether you should query members
@ -271,6 +289,7 @@ class ShardInfo:
"""
return self._parent.ws.is_ratelimited()
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
@ -297,16 +316,20 @@ class AutoShardedClient(Client):
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)
if TYPE_CHECKING:
_connection: AutoShardedConnectionState
def __init__(self, *args: Any, loop: Optional[asyncio.AbstractEventLoop] = None, **kwargs: Any) -> None:
kwargs.pop("shard_id", None)
self.shard_ids: Optional[List[int]] = 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.')
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.')
raise ClientException("shard_ids parameter must be a list or a tuple.")
# instead of a single websocket, we have multiple
# the key is the shard_id
@ -315,18 +338,24 @@ class AutoShardedClient(Client):
self._connection._get_client = lambda: self
self.__queue = asyncio.PriorityQueue()
def _get_websocket(self, guild_id=None, *, shard_id=None):
def _get_websocket(self, guild_id: Optional[int] = None, *, shard_id: Optional[int] = None) -> DiscordWebSocket:
if shard_id is None:
shard_id = (guild_id >> 22) % self.shard_count
# guild_id won't be None if shard_id is None and shard_count won't be None here
shard_id = (guild_id >> 22) % self.shard_count # type: ignore
return self.__shards[shard_id].ws
def _get_state(self, **options):
return AutoShardedConnectionState(dispatch=self.dispatch,
handlers=self._handlers,
hooks=self._hooks, http=self.http, loop=self.loop, **options)
def _get_state(self, **options: Any) -> AutoShardedConnectionState:
return AutoShardedConnectionState(
dispatch=self.dispatch,
handlers=self._handlers,
hooks=self._hooks,
http=self.http,
loop=self.loop,
**options,
)
@property
def latency(self):
def latency(self) -> float:
""":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
@ -334,18 +363,18 @@ class AutoShardedClient(Client):
:attr:`latencies` property. Returns ``nan`` if there are no shards ready.
"""
if not self.__shards:
return float('nan')
return float("nan")
return sum(latency for _, latency in self.latencies) / len(self.__shards)
@property
def latencies(self):
def latencies(self) -> List[Tuple[int, float]]:
"""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()]
def get_shard(self, shard_id):
def get_shard(self, shard_id: int) -> Optional[ShardInfo]:
"""Optional[:class:`ShardInfo`]: Gets the shard information at a given shard ID or ``None`` if not found."""
try:
parent = self.__shards[shard_id]
@ -355,52 +384,16 @@ class AutoShardedClient(Client):
return ShardInfo(parent, self.shard_count)
@property
def shards(self):
def shards(self) -> Dict[int, ShardInfo]:
"""Mapping[int, :class:`ShardInfo`]: Returns a mapping of shard IDs to their respective info object."""
return { shard_id: ShardInfo(parent, self.shard_count) for shard_id, parent in self.__shards.items() }
return {shard_id: ShardInfo(parent, self.shard_count) for shard_id, parent in self.__shards.items()}
@utils.deprecated('Guild.chunk')
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``.
.. warning::
This method is deprecated. Use :meth:`Guild.chunk` instead.
Parameters
-----------
\*guilds: :class:`Guild`
An argument list of guilds to request offline members for.
Raises
-------
InvalidArgument
If any guild is unavailable in the collection.
"""
if any(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):
for guild in sub_guilds:
await self._connection.chunk_guild(guild)
async def launch_shard(self, gateway, shard_id, *, initial=False):
async def launch_shard(self, gateway: str, shard_id: int, *, initial: bool = False) -> None:
try:
coro = DiscordWebSocket.from_client(self, initial=initial, gateway=gateway, shard_id=shard_id)
ws = await asyncio.wait_for(coro, timeout=180.0)
except Exception:
log.exception('Failed to connect for shard_id: %s. Retrying...', shard_id)
_log.exception("Failed to connect for shard_id: %s. Retrying...", shard_id)
await asyncio.sleep(5.0)
return await self.launch_shard(gateway, shard_id)
@ -408,7 +401,7 @@ class AutoShardedClient(Client):
self.__shards[shard_id] = ret = Shard(ws, self, self.__queue.put_nowait)
ret.launch()
async def launch_shards(self):
async def launch_shards(self) -> None:
if self.shard_count is None:
self.shard_count, gateway = await self.http.get_bot_gateway()
else:
@ -425,7 +418,7 @@ class AutoShardedClient(Client):
self._connection.shards_launched.set()
async def connect(self, *, reconnect=True):
async def connect(self, *, reconnect: bool = True) -> None:
self._reconnect = reconnect
await self.launch_shards()
@ -449,7 +442,7 @@ class AutoShardedClient(Client):
elif item.type == EventType.clean_close:
return
async def close(self):
async def close(self) -> None:
"""|coro|
Closes the connection to Discord.
@ -461,7 +454,7 @@ class AutoShardedClient(Client):
for vc in self.voice_clients:
try:
await vc.disconnect()
await vc.disconnect(force=True)
except Exception:
pass
@ -472,7 +465,13 @@ class AutoShardedClient(Client):
await self.http.close()
self.__queue.put_nowait(EventItem(EventType.clean_close, None, None))
async def change_presence(self, *, activity=None, status=None, afk=False, shard_id=None):
async def change_presence(
self,
*,
activity: Optional[BaseActivity] = None,
status: Optional[Status] = None,
shard_id: int = None,
) -> None:
"""|coro|
Changes the client's presence.
@ -482,6 +481,9 @@ class AutoShardedClient(Client):
game = discord.Game("with the API")
await client.change_presence(status=discord.Status.idle, activity=game)
.. versionchanged:: 2.0
Removed the ``afk`` keyword-only parameter.
Parameters
----------
activity: Optional[:class:`BaseActivity`]
@ -489,10 +491,6 @@ class AutoShardedClient(Client):
status: Optional[:class:`Status`]
Indicates what status to change to. If ``None``, then
:attr:`Status.online` is used.
afk: :class:`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[:class:`int`]
The shard_id to change the presence to. If not specified
or ``None``, then it will change the presence of every
@ -505,23 +503,23 @@ class AutoShardedClient(Client):
"""
if status is None:
status = 'online'
status_value = "online"
status_enum = Status.online
elif status is Status.offline:
status = 'invisible'
status_value = "invisible"
status_enum = Status.offline
else:
status_enum = status
status = str(status)
status_value = str(status)
if shard_id is None:
for shard in self.__shards.values():
await shard.ws.change_presence(activity=activity, status=status, afk=afk)
await shard.ws.change_presence(activity=activity, status=status_value)
guilds = self._connection.guilds
else:
shard = self.__shards[shard_id]
await shard.ws.change_presence(activity=activity, status=status, afk=afk)
await shard.ws.change_presence(activity=activity, status=status_value)
guilds = [g for g in self._connection.guilds if g.shard_id == shard_id]
activities = () if activity is None else (activity,)
@ -530,10 +528,11 @@ class AutoShardedClient(Client):
if me is None:
continue
me.activities = activities
# Member.activities is typehinted as Tuple[ActivityType, ...], we may be setting it as Tuple[BaseActivity, ...]
me.activities = activities # type: ignore
me.status = status_enum
def is_ws_ratelimited(self):
def is_ws_ratelimited(self) -> bool:
""":class:`bool`: Whether the websocket is currently rate limited.
This can be useful to know when deciding whether you should query members

180
discord/stage_instance.py Normal file
View File

@ -0,0 +1,180 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import Optional, TYPE_CHECKING
from .utils import MISSING, cached_slot_property
from .mixins import Hashable
from .errors import InvalidArgument
from .enums import StagePrivacyLevel, try_enum
__all__ = ("StageInstance",)
if TYPE_CHECKING:
from .types.channel import StageInstance as StageInstancePayload
from .state import ConnectionState
from .channel import StageChannel
from .guild import Guild
class StageInstance(Hashable):
"""Represents a stage instance of a stage channel in a guild.
.. versionadded:: 2.0
.. container:: operations
.. describe:: x == y
Checks if two stage instances are equal.
.. describe:: x != y
Checks if two stage instances are not equal.
.. describe:: hash(x)
Returns the stage instance's hash.
.. describe:: int(x)
Returns the stage instance's ID.
Attributes
-----------
id: :class:`int`
The stage instance's ID.
guild: :class:`Guild`
The guild that the stage instance is running in.
channel_id: :class:`int`
The ID of the channel that the stage instance is running in.
topic: :class:`str`
The topic of the stage instance.
privacy_level: :class:`StagePrivacyLevel`
The privacy level of the stage instance.
discoverable_disabled: :class:`bool`
Whether discoverability for the stage instance is disabled.
"""
__slots__ = (
"_state",
"id",
"guild",
"channel_id",
"topic",
"privacy_level",
"discoverable_disabled",
"_cs_channel",
)
def __init__(self, *, state: ConnectionState, guild: Guild, data: StageInstancePayload) -> None:
self._state = state
self.guild = guild
self._update(data)
def _update(self, data: StageInstancePayload):
self.id: int = int(data["id"])
self.channel_id: int = int(data["channel_id"])
self.topic: str = data["topic"]
self.privacy_level: StagePrivacyLevel = try_enum(StagePrivacyLevel, data["privacy_level"])
self.discoverable_disabled: bool = data.get("discoverable_disabled", False)
def __repr__(self) -> str:
return f"<StageInstance id={self.id} guild={self.guild!r} channel_id={self.channel_id} topic={self.topic!r}>"
@cached_slot_property("_cs_channel")
def channel(self) -> Optional[StageChannel]:
"""Optional[:class:`StageChannel`]: The channel that stage instance is running in."""
# the returned channel will always be a StageChannel or None
return self._state.get_channel(self.channel_id) # type: ignore
def is_public(self) -> bool:
return self.privacy_level is StagePrivacyLevel.public
async def edit(
self, *, topic: str = MISSING, privacy_level: StagePrivacyLevel = MISSING, reason: Optional[str] = None
) -> None:
"""|coro|
Edits the stage instance.
You must have the :attr:`~Permissions.manage_channels` permission to
use this.
Parameters
-----------
topic: :class:`str`
The stage instance's new topic.
privacy_level: :class:`StagePrivacyLevel`
The stage instance's new privacy level.
reason: :class:`str`
The reason the stage instance was edited. Shows up on the audit log.
Raises
------
InvalidArgument
If the ``privacy_level`` parameter is not the proper type.
Forbidden
You do not have permissions to edit the stage instance.
HTTPException
Editing a stage instance failed.
"""
payload = {}
if topic is not MISSING:
payload["topic"] = topic
if privacy_level is not MISSING:
if not isinstance(privacy_level, StagePrivacyLevel):
raise InvalidArgument("privacy_level field must be of type PrivacyLevel")
payload["privacy_level"] = privacy_level.value
if payload:
await self._state.http.edit_stage_instance(self.channel_id, **payload, reason=reason)
async def delete(self, *, reason: Optional[str] = None) -> None:
"""|coro|
Deletes the stage instance.
You must have the :attr:`~Permissions.manage_channels` permission to
use this.
Parameters
-----------
reason: :class:`str`
The reason the stage instance was deleted. Shows up on the audit log.
Raises
------
Forbidden
You do not have permissions to delete the stage instance.
HTTPException
Deleting the stage instance failed.
"""
await self._state.http.delete_stage_instance(self.channel_id, reason=reason)

File diff suppressed because it is too large Load Diff

View File

@ -22,17 +22,228 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Literal, TYPE_CHECKING, List, Optional, Tuple, Type, Union
import unicodedata
from .mixins import Hashable
from .asset import Asset
from .utils import snowflake_time
from .enums import StickerType, try_enum
from .asset import Asset, AssetMixin
from .utils import cached_slot_property, find, snowflake_time, get, MISSING
from .errors import InvalidData
from .enums import StickerType, StickerFormatType, try_enum
__all__ = (
'Sticker',
"StickerPack",
"StickerItem",
"Sticker",
"StandardSticker",
"GuildSticker",
)
class Sticker(Hashable):
"""Represents a sticker
if TYPE_CHECKING:
import datetime
from .state import ConnectionState
from .user import User
from .guild import Guild
from .types.sticker import (
StickerPack as StickerPackPayload,
StickerItem as StickerItemPayload,
Sticker as StickerPayload,
StandardSticker as StandardStickerPayload,
GuildSticker as GuildStickerPayload,
ListPremiumStickerPacks as ListPremiumStickerPacksPayload,
EditGuildSticker,
)
class StickerPack(Hashable):
"""Represents a sticker pack.
.. versionadded:: 2.0
.. container:: operations
.. describe:: str(x)
Returns the name of the sticker pack.
.. describe:: hash(x)
Returns the hash of the sticker pack.
.. describe:: int(x)
Returns the ID of the sticker pack.
.. describe:: x == y
Checks if the sticker pack is equal to another sticker pack.
.. describe:: x != y
Checks if the sticker pack is not equal to another sticker pack.
Attributes
-----------
name: :class:`str`
The name of the sticker pack.
description: :class:`str`
The description of the sticker pack.
id: :class:`int`
The id of the sticker pack.
stickers: List[:class:`StandardSticker`]
The stickers of this sticker pack.
sku_id: :class:`int`
The SKU ID of the sticker pack.
cover_sticker_id: :class:`int`
The ID of the sticker used for the cover of the sticker pack.
cover_sticker: :class:`StandardSticker`
The sticker used for the cover of the sticker pack.
"""
__slots__ = (
"_state",
"id",
"stickers",
"name",
"sku_id",
"cover_sticker_id",
"cover_sticker",
"description",
"_banner",
)
def __init__(self, *, state: ConnectionState, data: StickerPackPayload) -> None:
self._state: ConnectionState = state
self._from_data(data)
def _from_data(self, data: StickerPackPayload) -> None:
self.id: int = int(data["id"])
stickers = data["stickers"]
self.stickers: List[StandardSticker] = [
StandardSticker(state=self._state, data=sticker) for sticker in stickers
]
self.name: str = data["name"]
self.sku_id: int = int(data["sku_id"])
self.cover_sticker_id: int = int(data["cover_sticker_id"])
self.cover_sticker: StandardSticker = get(self.stickers, id=self.cover_sticker_id) # type: ignore
self.description: str = data["description"]
self._banner: int = int(data["banner_asset_id"])
@property
def banner(self) -> Asset:
""":class:`Asset`: The banner asset of the sticker pack."""
return Asset._from_sticker_banner(self._state, self._banner)
def __repr__(self) -> str:
return f"<StickerPack id={self.id} name={self.name!r} description={self.description!r}>"
def __str__(self) -> str:
return self.name
class _StickerTag(Hashable, AssetMixin):
__slots__ = ()
id: int
format: StickerFormatType
async def read(self) -> bytes:
"""|coro|
Retrieves the content of this sticker as a :class:`bytes` object.
.. note::
Stickers that use the :attr:`StickerFormatType.lottie` format cannot be read.
Raises
------
HTTPException
Downloading the asset failed.
NotFound
The asset was deleted.
TypeError
The sticker is a lottie type.
Returns
-------
:class:`bytes`
The content of the asset.
"""
if self.format is StickerFormatType.lottie:
raise TypeError('Cannot read stickers of format "lottie".')
return await super().read()
class StickerItem(_StickerTag):
"""Represents a sticker item.
.. versionadded:: 2.0
.. container:: operations
.. describe:: str(x)
Returns the name of the sticker item.
.. describe:: x == y
Checks if the sticker item is equal to another sticker item.
.. describe:: x != y
Checks if the sticker item is not equal to another sticker item.
Attributes
-----------
name: :class:`str`
The sticker's name.
id: :class:`int`
The id of the sticker.
format: :class:`StickerFormatType`
The format for the sticker's image.
url: :class:`str`
The URL for the sticker's image.
"""
__slots__ = ("_state", "name", "id", "format", "url")
def __init__(self, *, state: ConnectionState, data: StickerItemPayload):
self._state: ConnectionState = state
self.name: str = data["name"]
self.id: int = int(data["id"])
self.format: StickerFormatType = try_enum(StickerFormatType, data["format_type"])
self.url: str = f"{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}"
def __repr__(self) -> str:
return f"<StickerItem id={self.id} name={self.name!r} format={self.format}>"
def __str__(self) -> str:
return self.name
async def fetch(self) -> Union[Sticker, StandardSticker, GuildSticker]:
"""|coro|
Attempts to retrieve the full sticker data of the sticker item.
Raises
--------
HTTPException
Retrieving the sticker failed.
Returns
--------
Union[:class:`StandardSticker`, :class:`GuildSticker`]
The retrieved sticker.
"""
data: StickerPayload = await self._state.http.get_sticker(self.id)
cls, _ = _sticker_factory(data["type"]) # type: ignore
return cls(state=self._state, data=data)
class Sticker(_StickerTag):
"""Represents a sticker.
.. versionadded:: 1.6
@ -40,102 +251,285 @@ class Sticker(Hashable):
.. describe:: str(x)
Returns the name of the sticker
Returns the name of the sticker.
.. describe:: x == y
Checks if the sticker is equal to another sticker
Checks if the sticker is equal to another sticker.
.. describe:: x != y
Checks if the sticker is not equal to another sticker
Checks if the sticker is not equal to another sticker.
Attributes
----------
name: :class:`str`
The sticker's name
The sticker's name.
id: :class:`int`
The id of the sticker
The id of the sticker.
description: :class:`str`
The description of the sticker
The description of the sticker.
pack_id: :class:`int`
The id of the sticker's pack
format: :class:`StickerType`
The format for the sticker's image
image: :class:`str`
The sticker's image
tags: List[:class:`str`]
A list of tags for the sticker
preview_image: Optional[:class:`str`]
The sticker's preview asset hash
The id of the sticker's pack.
format: :class:`StickerFormatType`
The format for the sticker's image.
url: :class:`str`
The URL for the sticker's image.
"""
__slots__ = ('_state', 'id', 'name', 'description', 'pack_id', 'format', 'image', 'tags', 'preview_image')
def __init__(self, *, state, data):
self._state = state
self.id = int(data['id'])
self.name = data['name']
self.description = data['description']
self.pack_id = int(data['pack_id'])
self.format = try_enum(StickerType, data['format_type'])
self.image = data['asset']
__slots__ = ("_state", "id", "name", "description", "format", "url")
try:
self.tags = [tag.strip() for tag in data['tags'].split(',')]
except KeyError:
self.tags = []
def __init__(self, *, state: ConnectionState, data: StickerPayload) -> None:
self._state: ConnectionState = state
self._from_data(data)
self.preview_image = data.get('preview_asset')
def _from_data(self, data: StickerPayload) -> None:
self.id: int = int(data["id"])
self.name: str = data["name"]
self.description: str = data["description"]
self.format: StickerFormatType = try_enum(StickerFormatType, data["format_type"])
self.url: str = f"{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}"
def __repr__(self):
return '<{0.__class__.__name__} id={0.id} name={0.name!r}>'.format(self)
def __repr__(self) -> str:
return f"<Sticker id={self.id} name={self.name!r}>"
def __str__(self):
def __str__(self) -> str:
return self.name
@property
def created_at(self):
def created_at(self) -> datetime.datetime:
""":class:`datetime.datetime`: Returns the sticker's creation time in UTC."""
return snowflake_time(self.id)
@property
def image_url(self):
"""Returns an :class:`Asset` for the sticker's image.
.. note::
This will return ``None`` if the format is ``StickerType.lottie``
class StandardSticker(Sticker):
"""Represents a sticker that is found in a standard sticker pack.
.. versionadded:: 2.0
.. container:: operations
.. describe:: str(x)
Returns the name of the sticker.
.. describe:: x == y
Checks if the sticker is equal to another sticker.
.. describe:: x != y
Checks if the sticker is not equal to another sticker.
Attributes
----------
name: :class:`str`
The sticker's name.
id: :class:`int`
The id of the sticker.
description: :class:`str`
The description of the sticker.
pack_id: :class:`int`
The id of the sticker's pack.
format: :class:`StickerFormatType`
The format for the sticker's image.
tags: List[:class:`str`]
A list of tags for the sticker.
sort_value: :class:`int`
The sticker's sort order within its pack.
"""
__slots__ = ("sort_value", "pack_id", "type", "tags")
def _from_data(self, data: StandardStickerPayload) -> None:
super()._from_data(data)
self.sort_value: int = data["sort_value"]
self.pack_id: int = int(data["pack_id"])
self.type: StickerType = StickerType.standard
try:
self.tags: List[str] = [tag.strip() for tag in data["tags"].split(",")]
except KeyError:
self.tags = []
def __repr__(self) -> str:
return f"<StandardSticker id={self.id} name={self.name!r} pack_id={self.pack_id}>"
async def pack(self) -> StickerPack:
"""|coro|
Retrieves the sticker pack that this sticker belongs to.
Raises
--------
InvalidData
The corresponding sticker pack was not found.
HTTPException
Retrieving the sticker pack failed.
Returns
-------
Optional[:class:`Asset`]
The resulting CDN asset.
--------
:class:`StickerPack`
The retrieved sticker pack.
"""
return self.image_url_as()
data: ListPremiumStickerPacksPayload = await self._state.http.list_premium_sticker_packs()
packs = data["sticker_packs"]
pack = find(lambda d: int(d["id"]) == self.pack_id, packs)
def image_url_as(self, *, size=1024):
"""Optionally returns an :class:`Asset` for the sticker's image.
if pack:
return StickerPack(state=self._state, data=pack)
raise InvalidData(f"Could not find corresponding sticker pack for {self!r}")
The size must be a power of 2 between 16 and 4096.
.. note::
This will return ``None`` if the format is ``StickerType.lottie``.
class GuildSticker(Sticker):
"""Represents a sticker that belongs to a guild.
.. versionadded:: 2.0
.. container:: operations
.. describe:: str(x)
Returns the name of the sticker.
.. describe:: x == y
Checks if the sticker is equal to another sticker.
.. describe:: x != y
Checks if the sticker is not equal to another sticker.
Attributes
----------
name: :class:`str`
The sticker's name.
id: :class:`int`
The id of the sticker.
description: :class:`str`
The description of the sticker.
format: :class:`StickerFormatType`
The format for the sticker's image.
available: :class:`bool`
Whether this sticker is available for use.
guild_id: :class:`int`
The ID of the guild that this sticker is from.
user: Optional[:class:`User`]
The user that created this sticker. This can only be retrieved using :meth:`Guild.fetch_sticker` and
having the :attr:`~Permissions.manage_emojis_and_stickers` permission.
emoji: :class:`str`
The name of a unicode emoji that represents this sticker.
"""
__slots__ = ("available", "guild_id", "user", "emoji", "type", "_cs_guild")
def _from_data(self, data: GuildStickerPayload) -> None:
super()._from_data(data)
self.available: bool = data["available"]
self.guild_id: int = int(data["guild_id"])
user = data.get("user")
self.user: Optional[User] = self._state.store_user(user) if user else None
self.emoji: str = data["tags"]
self.type: StickerType = StickerType.guild
def __repr__(self) -> str:
return f"<GuildSticker name={self.name!r} id={self.id} guild_id={self.guild_id} user={self.user!r}>"
@cached_slot_property("_cs_guild")
def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: The guild that this sticker is from.
Could be ``None`` if the bot is not in the guild.
.. versionadded:: 2.0
"""
return self._state._get_guild(self.guild_id)
async def edit(
self,
*,
name: str = MISSING,
description: str = MISSING,
emoji: str = MISSING,
reason: Optional[str] = None,
) -> GuildSticker:
"""|coro|
Edits a :class:`GuildSticker` for the guild.
Parameters
-----------
size: :class:`int`
The size of the image to display.
name: :class:`str`
The sticker's new name. Must be at least 2 characters.
description: Optional[:class:`str`]
The sticker's new description. Can be ``None``.
emoji: :class:`str`
The name of a unicode emoji that represents the sticker's expression.
reason: :class:`str`
The reason for editing this sticker. Shows up on the audit log.
Raises
------
InvalidArgument
Invalid ``size``.
-------
Forbidden
You are not allowed to edit stickers.
HTTPException
An error occurred editing the sticker.
Returns
-------
Optional[:class:`Asset`]
The resulting CDN asset or ``None``.
--------
:class:`GuildSticker`
The newly modified sticker.
"""
if self.format is StickerType.lottie:
return None
payload: EditGuildSticker = {}
return Asset._from_sticker_url(self._state, self, size=size)
if name is not MISSING:
payload["name"] = name
if description is not MISSING:
payload["description"] = description
if emoji is not MISSING:
try:
emoji = unicodedata.name(emoji)
except TypeError:
pass
else:
emoji = emoji.replace(" ", "_")
payload["tags"] = emoji
data: GuildStickerPayload = await self._state.http.modify_guild_sticker(self.guild_id, self.id, payload, reason)
return GuildSticker(state=self._state, data=data)
async def delete(self, *, reason: Optional[str] = None) -> None:
"""|coro|
Deletes the custom :class:`Sticker` from the guild.
You must have :attr:`~Permissions.manage_emojis_and_stickers` permission to
do this.
Parameters
-----------
reason: Optional[:class:`str`]
The reason for deleting this sticker. Shows up on the audit log.
Raises
-------
Forbidden
You are not allowed to delete stickers.
HTTPException
An error occurred deleting the sticker.
"""
await self._state.http.delete_guild_sticker(self.guild_id, self.id, reason)
def _sticker_factory(
sticker_type: Literal[1, 2]
) -> Tuple[Type[Union[StandardSticker, GuildSticker, Sticker]], StickerType]:
value = try_enum(StickerType, sticker_type)
if value == StickerType.standard:
return StandardSticker, value
elif value == StickerType.guild:
return GuildSticker, value
else:
return Sticker, value

View File

@ -22,16 +22,29 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from . import utils
from .user import BaseUser
from .asset import Asset
from .enums import TeamMembershipState, try_enum
from typing import TYPE_CHECKING, Optional, List
if TYPE_CHECKING:
from .state import ConnectionState
from .types.team import (
Team as TeamPayload,
TeamMember as TeamMemberPayload,
)
__all__ = (
'Team',
'TeamMember',
"Team",
"TeamMember",
)
class Team:
"""Represents an application team for a bot provided by Discord.
@ -41,8 +54,6 @@ class Team:
The team ID.
name: :class:`str`
The team name
icon: Optional[:class:`str`]
The icon hash, if it exists.
owner_id: :class:`int`
The team's owner ID.
members: List[:class:`TeamMember`]
@ -50,61 +61,34 @@ class Team:
.. versionadded:: 1.3
"""
__slots__ = ('_state', 'id', 'name', 'icon', 'owner_id', 'members')
def __init__(self, state, data):
self._state = state
__slots__ = ("_state", "id", "name", "_icon", "owner_id", "members")
self.id = utils._get_as_snowflake(data, 'id')
self.name = data['name']
self.icon = data['icon']
self.owner_id = utils._get_as_snowflake(data, 'owner_user_id')
self.members = [TeamMember(self, self._state, member) for member in data['members']]
def __init__(self, state: ConnectionState, data: TeamPayload):
self._state: ConnectionState = state
def __repr__(self):
return '<{0.__class__.__name__} id={0.id} name={0.name}>'.format(self)
self.id: int = int(data["id"])
self.name: str = data["name"]
self._icon: Optional[str] = data["icon"]
self.owner_id: Optional[int] = utils._get_as_snowflake(data, "owner_user_id")
self.members: List[TeamMember] = [TeamMember(self, self._state, member) for member in data["members"]]
def __repr__(self) -> str:
return f"<{self.__class__.__name__} id={self.id} name={self.name}>"
@property
def icon_url(self):
""":class:`.Asset`: Retrieves the team's icon asset.
This is equivalent to calling :meth:`icon_url_as` with
the default parameters ('webp' format and a size of 1024).
"""
return self.icon_url_as()
def icon_url_as(self, *, format='webp', size=1024):
"""Returns an :class:`Asset` for the icon the team has.
The format must be one of 'webp', 'jpeg', 'jpg' or 'png'.
The size must be a power of 2 between 16 and 4096.
.. versionadded:: 2.0
Parameters
-----------
format: :class:`str`
The format to attempt to convert the icon to. Defaults to 'webp'.
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_icon(self._state, self, 'team', format=format, size=size)
def icon(self) -> Optional[Asset]:
"""Optional[:class:`.Asset`]: Retrieves the team's icon asset, if any."""
if self._icon is None:
return None
return Asset._from_icon(self._state, self.id, self._icon, path="team")
@property
def owner(self):
def owner(self) -> Optional[TeamMember]:
"""Optional[:class:`TeamMember`]: The team's owner."""
return utils.get(self.members, id=self.owner_id)
class TeamMember(BaseUser):
"""Represents a team member in a team.
@ -145,14 +129,17 @@ class TeamMember(BaseUser):
membership_state: :class:`TeamMembershipState`
The membership state of the member (e.g. invited or accepted)
"""
__slots__ = BaseUser.__slots__ + ('team', 'membership_state', 'permissions')
def __init__(self, team, state, data):
self.team = team
self.membership_state = try_enum(TeamMembershipState, data['membership_state'])
self.permissions = data['permissions']
super().__init__(state=state, data=data['user'])
__slots__ = ("team", "membership_state", "permissions")
def __repr__(self):
return '<{0.__class__.__name__} id={0.id} name={0.name!r} ' \
'discriminator={0.discriminator!r} membership_state={0.membership_state!r}>'.format(self)
def __init__(self, team: Team, state: ConnectionState, data: TeamMemberPayload):
self.team: Team = team
self.membership_state: TeamMembershipState = try_enum(TeamMembershipState, data["membership_state"])
self.permissions: List[str] = data["permissions"]
super().__init__(state=state, data=data["user"])
def __repr__(self) -> str:
return (
f"<{self.__class__.__name__} id={self.id} name={self.name!r} "
f"discriminator={self.discriminator!r} membership_state={self.membership_state!r}>"
)

View File

@ -22,19 +22,28 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from .utils import parse_time, _get_as_snowflake, _bytes_to_base64_data
from __future__ import annotations
from typing import Any, Optional, TYPE_CHECKING
from .utils import parse_time, _get_as_snowflake, _bytes_to_base64_data, MISSING
from .enums import VoiceRegion
from .guild import Guild
__all__ = (
'Template',
)
__all__ = ("Template",)
if TYPE_CHECKING:
import datetime
from .types.template import Template as TemplatePayload
from .state import ConnectionState
from .user import User
class _FriendlyHttpAttributeErrorHelper:
__slots__ = ()
def __getattr__(self, attr):
raise AttributeError('PartialTemplateState does not support http methods.')
raise AttributeError("PartialTemplateState does not support http methods.")
class _PartialTemplateState:
def __init__(self, *, state):
@ -66,11 +75,15 @@ class _PartialTemplateState:
def _get_message(self, id):
return None
async def query_members(self, **kwargs):
def _get_guild(self, id):
return self.__state._get_guild(id)
async def query_members(self, **kwargs: Any):
return []
def __getattr__(self, attr):
raise AttributeError(f'PartialTemplateState does not support {attr!r}.')
raise AttributeError(f"PartialTemplateState does not support {attr!r}.")
class Template:
"""Represents a Discord template.
@ -96,40 +109,62 @@ class Template:
This is referred to as "last synced" in the official Discord client.
source_guild: :class:`Guild`
The source guild.
is_dirty: Optional[:class:`bool`]
Whether the template has unsynced changes.
.. versionadded:: 2.0
"""
def __init__(self, *, state, data):
__slots__ = (
"code",
"uses",
"name",
"description",
"creator",
"created_at",
"updated_at",
"source_guild",
"is_dirty",
"_state",
)
def __init__(self, *, state: ConnectionState, data: TemplatePayload) -> None:
self._state = state
self._store(data)
def _store(self, data):
self.code = data['code']
self.uses = data['usage_count']
self.name = data['name']
self.description = data['description']
creator_data = data.get('creator')
self.creator = None if creator_data is None else self._state.store_user(creator_data)
def _store(self, data: TemplatePayload) -> None:
self.code: str = data["code"]
self.uses: int = data["usage_count"]
self.name: str = data["name"]
self.description: Optional[str] = data["description"]
creator_data = data.get("creator")
self.creator: Optional[User] = None if creator_data is None else self._state.create_user(creator_data)
self.created_at = parse_time(data.get('created_at'))
self.updated_at = parse_time(data.get('updated_at'))
self.created_at: Optional[datetime.datetime] = parse_time(data.get("created_at"))
self.updated_at: Optional[datetime.datetime] = parse_time(data.get("updated_at"))
id = _get_as_snowflake(data, 'source_guild_id')
guild = self._state._get_guild(id)
guild_id = int(data["source_guild_id"])
guild: Optional[Guild] = self._state._get_guild(guild_id)
self.source_guild: Guild
if guild is None:
source_serialised = data['serialized_source_guild']
source_serialised['id'] = id
source_serialised = data["serialized_source_guild"]
source_serialised["id"] = guild_id
state = _PartialTemplateState(state=self._state)
guild = Guild(data=source_serialised, state=state)
# Guild expects a ConnectionState, we're passing a _PartialTemplateState
self.source_guild = Guild(data=source_serialised, state=state) # type: ignore
else:
self.source_guild = guild
self.source_guild = guild
self.is_dirty: Optional[bool] = data.get("is_dirty", None)
def __repr__(self):
return '<Template code={0.code!r} uses={0.uses} name={0.name!r}' \
' creator={0.creator!r} source_guild={0.source_guild!r}>'.format(self)
def __repr__(self) -> str:
return (
f"<Template code={self.code!r} uses={self.uses} name={self.name!r}"
f" creator={self.creator!r} source_guild={self.source_guild!r} is_dirty={self.is_dirty}>"
)
async def create_guild(self, name, region=None, icon=None):
async def create_guild(self, name: str, region: Optional[VoiceRegion] = None, icon: Any = None) -> Guild:
"""|coro|
Creates a :class:`.Guild` using the template.
@ -169,7 +204,7 @@ class Template:
data = await self._state.http.create_from_template(self.code, name, region_value, icon)
return Guild(data=data, state=self._state)
async def sync(self):
async def sync(self) -> Template:
"""|coro|
Sync the template to the guild's current state.
@ -179,6 +214,9 @@ class Template:
.. versionadded:: 1.7
.. versionchanged:: 2.0
The template is no longer edited in-place, instead it is returned.
Raises
-------
HTTPException
@ -187,12 +225,22 @@ class Template:
You don't have permissions to edit the template.
NotFound
This template does not exist.
Returns
--------
:class:`Template`
The newly edited template.
"""
data = await self._state.http.sync_template(self.source_guild.id, self.code)
self._store(data)
return Template(state=self._state, data=data)
async def edit(self, **kwargs):
async def edit(
self,
*,
name: str = MISSING,
description: Optional[str] = MISSING,
) -> Template:
"""|coro|
Edit the template metadata.
@ -202,12 +250,15 @@ class Template:
.. versionadded:: 1.7
.. versionchanged:: 2.0
The template is no longer edited in-place, instead it is returned.
Parameters
------------
name: Optional[:class:`str`]
name: :class:`str`
The template's new name.
description: Optional[:class:`str`]
The template's description.
The template's new description.
Raises
-------
@ -217,11 +268,23 @@ class Template:
You don't have permissions to edit the template.
NotFound
This template does not exist.
"""
data = await self._state.http.edit_template(self.source_guild.id, self.code, kwargs)
self._store(data)
async def delete(self):
Returns
--------
:class:`Template`
The newly edited template.
"""
payload = {}
if name is not MISSING:
payload["name"] = name
if description is not MISSING:
payload["description"] = description
data = await self._state.http.edit_template(self.source_guild.id, self.code, payload)
return Template(state=self._state, data=data)
async def delete(self) -> None:
"""|coro|
Delete the template.
@ -241,3 +304,11 @@ class Template:
This template does not exist.
"""
await self._state.http.delete_template(self.source_guild.id, self.code)
@property
def url(self) -> str:
""":class:`str`: The template url.
.. versionadded:: 2.0
"""
return f"https://discord.new/{self.code}"

846
discord/threads.py Normal file
View File

@ -0,0 +1,846 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import Callable, Dict, Iterable, List, Optional, Union, TYPE_CHECKING
import time
import asyncio
from .mixins import Hashable
from .abc import Messageable
from .enums import ChannelType, try_enum
from .errors import ClientException
from .utils import MISSING, parse_time, _get_as_snowflake
__all__ = (
"Thread",
"ThreadMember",
)
if TYPE_CHECKING:
from .types.threads import (
Thread as ThreadPayload,
ThreadMember as ThreadMemberPayload,
ThreadMetadata,
ThreadArchiveDuration,
)
from .types.snowflake import SnowflakeList
from .guild import Guild
from .channel import TextChannel, CategoryChannel
from .member import Member
from .message import Message, PartialMessage
from .abc import Snowflake, SnowflakeTime
from .role import Role
from .permissions import Permissions
from .state import ConnectionState
class Thread(Messageable, Hashable):
"""Represents a Discord thread.
.. container:: operations
.. describe:: x == y
Checks if two threads are equal.
.. describe:: x != y
Checks if two threads are not equal.
.. describe:: hash(x)
Returns the thread's hash.
.. describe:: int(x)
Returns the thread's ID.
.. describe:: str(x)
Returns the thread's name.
.. versionadded:: 2.0
Attributes
-----------
name: :class:`str`
The thread name.
guild: :class:`Guild`
The guild the thread belongs to.
id: :class:`int`
The thread ID.
parent_id: :class:`int`
The parent :class:`TextChannel` ID this thread belongs to.
owner_id: :class:`int`
The user's ID that created this thread.
last_message_id: Optional[:class:`int`]
The last message ID of the message sent to this thread. It may
*not* point to an existing or valid message.
slowmode_delay: :class:`int`
The number of seconds a member must wait between sending messages
in this thread. A value of `0` denotes that it is disabled.
Bots and users with :attr:`~Permissions.manage_channels` or
:attr:`~Permissions.manage_messages` bypass slowmode.
message_count: :class:`int`
An approximate number of messages in this thread. This caps at 50.
member_count: :class:`int`
An approximate number of members in this thread. This caps at 50.
me: Optional[:class:`ThreadMember`]
A thread member representing yourself, if you've joined the thread.
This could not be available.
archived: :class:`bool`
Whether the thread is archived.
locked: :class:`bool`
Whether the thread is locked.
invitable: :class:`bool`
Whether non-moderators can add other non-moderators to this thread.
This is always ``True`` for public threads.
archiver_id: Optional[:class:`int`]
The user's ID that archived this thread.
auto_archive_duration: :class:`int`
The duration in minutes until the thread is automatically archived due to inactivity.
Usually a value of 60, 1440, 4320 and 10080.
archive_timestamp: :class:`datetime.datetime`
An aware timestamp of when the thread's archived status was last updated in UTC.
"""
__slots__ = (
"name",
"id",
"guild",
"_type",
"_state",
"_members",
"owner_id",
"parent_id",
"last_message_id",
"message_count",
"member_count",
"slowmode_delay",
"me",
"locked",
"archived",
"invitable",
"archiver_id",
"auto_archive_duration",
"archive_timestamp",
)
def __init__(self, *, guild: Guild, state: ConnectionState, data: ThreadPayload):
self._state: ConnectionState = state
self.guild = guild
self._members: Dict[int, ThreadMember] = {}
self._from_data(data)
async def _get_channel(self):
return self
def __repr__(self) -> str:
return (
f"<Thread id={self.id!r} name={self.name!r} parent={self.parent}"
f" owner_id={self.owner_id!r} locked={self.locked} archived={self.archived}>"
)
def __str__(self) -> str:
return self.name
def _from_data(self, data: ThreadPayload):
self.id = int(data["id"])
self.parent_id = int(data["parent_id"])
self.owner_id = int(data["owner_id"])
self.name = data["name"]
self._type = try_enum(ChannelType, data["type"])
self.last_message_id = _get_as_snowflake(data, "last_message_id")
self.slowmode_delay = data.get("rate_limit_per_user", 0)
self.message_count = data["message_count"]
self.member_count = data["member_count"]
self._unroll_metadata(data["thread_metadata"])
try:
member = data["member"]
except KeyError:
self.me = None
else:
self.me = ThreadMember(self, member)
def _unroll_metadata(self, data: ThreadMetadata):
self.archived = data["archived"]
self.archiver_id = _get_as_snowflake(data, "archiver_id")
self.auto_archive_duration = data["auto_archive_duration"]
self.archive_timestamp = parse_time(data["archive_timestamp"])
self.locked = data.get("locked", False)
self.invitable = data.get("invitable", True)
def _update(self, data):
try:
self.name = data["name"]
except KeyError:
pass
self.slowmode_delay = data.get("rate_limit_per_user", 0)
try:
self._unroll_metadata(data["thread_metadata"])
except KeyError:
pass
@property
def type(self) -> ChannelType:
""":class:`ChannelType`: The channel's Discord type."""
return self._type
@property
def parent(self) -> Optional[TextChannel]:
"""Optional[:class:`TextChannel`]: The parent channel this thread belongs to."""
return self.guild.get_channel(self.parent_id) # type: ignore
@property
def owner(self) -> Optional[Member]:
"""Optional[:class:`Member`]: The member this thread belongs to."""
return self.guild.get_member(self.owner_id)
@property
def mention(self) -> str:
""":class:`str`: The string that allows you to mention the thread."""
return f"<#{self.id}>"
@property
def members(self) -> List[ThreadMember]:
"""List[:class:`ThreadMember`]: A list of thread members in this thread.
This requires :attr:`Intents.members` to be properly filled. Most of the time however,
this data is not provided by the gateway and a call to :meth:`fetch_members` is
needed.
"""
return list(self._members.values())
@property
def last_message(self) -> Optional[Message]:
"""Fetches the last message from this channel in cache.
The message might not be valid or point to an existing message.
.. admonition:: Reliable Fetching
:class: helpful
For a slightly more reliable method of fetching the
last message, consider using either :meth:`history`
or :meth:`fetch_message` with the :attr:`last_message_id`
attribute.
Returns
---------
Optional[:class:`Message`]
The last message in this channel or ``None`` if not found.
"""
return self._state._get_message(self.last_message_id) if self.last_message_id else None
@property
def category(self) -> Optional[CategoryChannel]:
"""The category channel the parent channel belongs to, if applicable.
Raises
-------
ClientException
The parent channel was not cached and returned ``None``.
Returns
-------
Optional[:class:`CategoryChannel`]
The parent channel's category.
"""
parent = self.parent
if parent is None:
raise ClientException("Parent channel not found")
return parent.category
@property
def category_id(self) -> Optional[int]:
"""The category channel ID the parent channel belongs to, if applicable.
Raises
-------
ClientException
The parent channel was not cached and returned ``None``.
Returns
-------
Optional[:class:`int`]
The parent channel's category ID.
"""
parent = self.parent
if parent is None:
raise ClientException("Parent channel not found")
return parent.category_id
def is_private(self) -> bool:
""":class:`bool`: Whether the thread is a private thread.
A private thread is only viewable by those that have been explicitly
invited or have :attr:`~.Permissions.manage_threads`.
"""
return self._type is ChannelType.private_thread
def is_news(self) -> bool:
""":class:`bool`: Whether the thread is a news thread.
A news thread is a thread that has a parent that is a news channel,
i.e. :meth:`.TextChannel.is_news` is ``True``.
"""
return self._type is ChannelType.news_thread
def is_nsfw(self) -> bool:
""":class:`bool`: Whether the thread is NSFW or not.
An NSFW thread is a thread that has a parent that is an NSFW channel,
i.e. :meth:`.TextChannel.is_nsfw` is ``True``.
"""
parent = self.parent
return parent is not None and parent.is_nsfw()
def permissions_for(self, obj: Union[Member, Role], /) -> Permissions:
"""Handles permission resolution for the :class:`~discord.Member`
or :class:`~discord.Role`.
Since threads do not have their own permissions, they inherit them
from the parent channel. This is a convenience method for
calling :meth:`~discord.TextChannel.permissions_for` on the
parent channel.
Parameters
----------
obj: Union[:class:`~discord.Member`, :class:`~discord.Role`]
The object to resolve permissions for. This could be either
a member or a role. If it's a role then member overwrites
are not computed.
Raises
-------
ClientException
The parent channel was not cached and returned ``None``
Returns
-------
:class:`~discord.Permissions`
The resolved permissions for the member or role.
"""
parent = self.parent
if parent is None:
raise ClientException("Parent channel not found")
return parent.permissions_for(obj)
async def delete_messages(self, messages: Iterable[Snowflake]) -> None:
"""|coro|
Deletes a list of messages. This is similar to :meth:`Message.delete`
except it bulk deletes multiple messages.
As a special case, if the number of messages is 0, then nothing
is done. If the number of messages is 1 then single message
delete is done. If it's more than two, then bulk delete is used.
You cannot bulk delete more than 100 messages or messages that
are older than 14 days old.
You must have the :attr:`~Permissions.manage_messages` permission to
use this.
Usable only by bot accounts.
Parameters
-----------
messages: Iterable[:class:`abc.Snowflake`]
An iterable of messages denoting which ones to bulk delete.
Raises
------
ClientException
The number of messages to delete was more than 100.
Forbidden
You do not have proper permissions to delete the messages or
you're not using a bot account.
NotFound
If single delete, then the message was already deleted.
HTTPException
Deleting the messages failed.
"""
if not isinstance(messages, (list, tuple)):
messages = list(messages)
if len(messages) == 0:
return # do nothing
if len(messages) == 1:
message_id = messages[0].id
await self._state.http.delete_message(self.id, message_id)
return
if len(messages) > 100:
raise ClientException("Can only bulk delete messages up to 100 messages")
message_ids: SnowflakeList = [m.id for m in messages]
await self._state.http.delete_messages(self.id, message_ids)
async def purge(
self,
*,
limit: Optional[int] = 100,
check: Callable[[Message], bool] = MISSING,
before: Optional[SnowflakeTime] = None,
after: Optional[SnowflakeTime] = None,
around: Optional[SnowflakeTime] = None,
oldest_first: Optional[bool] = False,
bulk: bool = True,
) -> List[Message]:
"""|coro|
Purges a list of messages that meet the criteria given by the predicate
``check``. If a ``check`` is not provided then all messages are deleted
without discrimination.
You must have the :attr:`~Permissions.manage_messages` permission to
delete messages even if they are your own (unless you are a user
account). The :attr:`~Permissions.read_message_history` permission is
also needed to retrieve message history.
Examples
---------
Deleting bot's messages ::
def is_me(m):
return m.author == client.user
deleted = await thread.purge(limit=100, check=is_me)
await thread.send(f'Deleted {len(deleted)} message(s)')
Parameters
-----------
limit: Optional[:class:`int`]
The number of messages to search through. This is not the number
of messages that will be deleted, though it can be.
check: Callable[[:class:`Message`], :class:`bool`]
The function used to check if a message should be deleted.
It must take a :class:`Message` as its sole parameter.
before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
Same as ``before`` in :meth:`history`.
after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
Same as ``after`` in :meth:`history`.
around: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
Same as ``around`` in :meth:`history`.
oldest_first: Optional[:class:`bool`]
Same as ``oldest_first`` in :meth:`history`.
bulk: :class:`bool`
If ``True``, use bulk delete. Setting this to ``False`` is useful for mass-deleting
a bot's own messages without :attr:`Permissions.manage_messages`. When ``True``, will
fall back to single delete if messages are older than two weeks.
Raises
-------
Forbidden
You do not have proper permissions to do the actions required.
HTTPException
Purging the messages failed.
Returns
--------
List[:class:`.Message`]
The list of messages that were deleted.
"""
if check is MISSING:
check = lambda m: True
iterator = self.history(limit=limit, before=before, after=after, oldest_first=oldest_first, around=around)
ret: List[Message] = []
count = 0
minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22
async def _single_delete_strategy(messages: Iterable[Message]):
for m in messages:
await m.delete()
strategy = self.delete_messages if bulk else _single_delete_strategy
async for message in iterator:
if count == 100:
to_delete = ret[-100:]
await strategy(to_delete)
count = 0
await asyncio.sleep(1)
if not check(message):
continue
if message.id < minimum_time:
# older than 14 days old
if count == 1:
await ret[-1].delete()
elif count >= 2:
to_delete = ret[-count:]
await strategy(to_delete)
count = 0
strategy = _single_delete_strategy
count += 1
ret.append(message)
# SOme messages remaining to poll
if count >= 2:
# more than 2 messages -> bulk delete
to_delete = ret[-count:]
await strategy(to_delete)
elif count == 1:
# delete a single message
await ret[-1].delete()
return ret
async def edit(
self,
*,
name: str = MISSING,
archived: bool = MISSING,
locked: bool = MISSING,
invitable: bool = MISSING,
slowmode_delay: int = MISSING,
auto_archive_duration: ThreadArchiveDuration = MISSING,
) -> Thread:
"""|coro|
Edits the thread.
Editing the thread requires :attr:`.Permissions.manage_threads`. The thread
creator can also edit ``name``, ``archived`` or ``auto_archive_duration``.
Note that if the thread is locked then only those with :attr:`.Permissions.manage_threads`
can unarchive a thread.
The thread must be unarchived to be edited.
Parameters
------------
name: :class:`str`
The new name of the thread.
archived: :class:`bool`
Whether to archive the thread or not.
locked: :class:`bool`
Whether to lock the thread or not.
invitable: :class:`bool`
Whether non-moderators can add other non-moderators to this thread.
Only available for private threads.
auto_archive_duration: :class:`int`
The new duration in minutes before a thread is automatically archived for inactivity.
Must be one of ``60``, ``1440``, ``4320``, or ``10080``.
slowmode_delay: :class:`int`
Specifies the slowmode rate limit for user in this thread, in seconds.
A value of ``0`` disables slowmode. The maximum value possible is ``21600``.
Raises
-------
Forbidden
You do not have permissions to edit the thread.
HTTPException
Editing the thread failed.
Returns
--------
:class:`Thread`
The newly edited thread.
"""
payload = {}
if name is not MISSING:
payload["name"] = str(name)
if archived is not MISSING:
payload["archived"] = archived
if auto_archive_duration is not MISSING:
payload["auto_archive_duration"] = auto_archive_duration
if locked is not MISSING:
payload["locked"] = locked
if invitable is not MISSING:
payload["invitable"] = invitable
if slowmode_delay is not MISSING:
payload["rate_limit_per_user"] = slowmode_delay
data = await self._state.http.edit_channel(self.id, **payload)
# The data payload will always be a Thread payload
return Thread(data=data, state=self._state, guild=self.guild) # type: ignore
async def join(self):
"""|coro|
Joins this thread.
You must have :attr:`~Permissions.send_messages_in_threads` to join a thread.
If the thread is private, :attr:`~Permissions.manage_threads` is also needed.
Raises
-------
Forbidden
You do not have permissions to join the thread.
HTTPException
Joining the thread failed.
"""
await self._state.http.join_thread(self.id)
async def leave(self):
"""|coro|
Leaves this thread.
Raises
-------
HTTPException
Leaving the thread failed.
"""
await self._state.http.leave_thread(self.id)
async def add_user(self, user: Snowflake):
"""|coro|
Adds a user to this thread.
You must have :attr:`~Permissions.send_messages` and :attr:`~Permissions.use_threads`
to add a user to a public thread. If the thread is private then :attr:`~Permissions.send_messages`
and either :attr:`~Permissions.use_private_threads` or :attr:`~Permissions.manage_messages`
is required to add a user to the thread.
Parameters
-----------
user: :class:`abc.Snowflake`
The user to add to the thread.
Raises
-------
Forbidden
You do not have permissions to add the user to the thread.
HTTPException
Adding the user to the thread failed.
"""
await self._state.http.add_user_to_thread(self.id, user.id)
async def remove_user(self, user: Snowflake):
"""|coro|
Removes a user from this thread.
You must have :attr:`~Permissions.manage_threads` or be the creator of the thread to remove a user.
Parameters
-----------
user: :class:`abc.Snowflake`
The user to add to the thread.
Raises
-------
Forbidden
You do not have permissions to remove the user from the thread.
HTTPException
Removing the user from the thread failed.
"""
await self._state.http.remove_user_from_thread(self.id, user.id)
async def fetch_members(self) -> List[ThreadMember]:
"""|coro|
Retrieves all :class:`ThreadMember` that are in this thread.
This requires :attr:`Intents.members` to get information about members
other than yourself.
Raises
-------
HTTPException
Retrieving the members failed.
Returns
--------
List[:class:`ThreadMember`]
All thread members in the thread.
"""
members = await self._state.http.get_thread_members(self.id)
return [ThreadMember(parent=self, data=data) for data in members]
async def delete(self):
"""|coro|
Deletes this thread.
You must have :attr:`~Permissions.manage_threads` to delete threads.
Raises
-------
Forbidden
You do not have permissions to delete this thread.
HTTPException
Deleting the thread failed.
"""
await self._state.http.delete_channel(self.id)
def get_partial_message(self, message_id: int, /) -> PartialMessage:
"""Creates a :class:`PartialMessage` from the message ID.
This is useful if you want to work with a message and only have its ID without
doing an unnecessary API call.
.. versionadded:: 2.0
Parameters
------------
message_id: :class:`int`
The message ID to create a partial message for.
Returns
---------
:class:`PartialMessage`
The partial message.
"""
from .message import PartialMessage
return PartialMessage(channel=self, id=message_id)
def _add_member(self, member: ThreadMember) -> None:
self._members[member.id] = member
def _pop_member(self, member_id: int) -> Optional[ThreadMember]:
return self._members.pop(member_id, None)
class ThreadMember(Hashable):
"""Represents a Discord thread member.
.. container:: operations
.. describe:: x == y
Checks if two thread members are equal.
.. describe:: x != y
Checks if two thread members are not equal.
.. describe:: hash(x)
Returns the thread member's hash.
.. describe:: int(x)
Returns the thread member's ID.
.. describe:: str(x)
Returns the thread member's name.
.. versionadded:: 2.0
Attributes
-----------
id: :class:`int`
The thread member's ID.
thread_id: :class:`int`
The thread's ID.
joined_at: :class:`datetime.datetime`
The time the member joined the thread in UTC.
"""
__slots__ = (
"id",
"thread_id",
"joined_at",
"flags",
"_state",
"parent",
)
def __init__(self, parent: Thread, data: ThreadMemberPayload):
self.parent = parent
self._state = parent._state
self._from_data(data)
def __repr__(self) -> str:
return f"<ThreadMember id={self.id} thread_id={self.thread_id} joined_at={self.joined_at!r}>"
def _from_data(self, data: ThreadMemberPayload):
try:
self.id = int(data["user_id"])
except KeyError:
assert self._state.self_id is not None
self.id = self._state.self_id
try:
self.thread_id = int(data["id"])
except KeyError:
self.thread_id = self.parent.id
self.joined_at = parse_time(data["join_timestamp"])
self.flags = data["flags"]
@property
def thread(self) -> Thread:
""":class:`Thread`: The thread this member belongs to."""
return self.parent
async def fetch_member(self) -> Member:
"""|coro|
Retrieves a :class:`Member` from the ThreadMember object.
.. note::
This method is an API call. If you have :attr:`Intents.members` and member cache enabled, consider :meth:`get_member` instead.
Raises
-------
Forbidden
You do not have access to the guild.
HTTPException
Fetching the member failed.
Returns
--------
:class:`Member`
The member.
"""
return await self.thread.guild.fetch_member(self.id)
def get_member(self) -> Optional[Member]:
"""
Get the :class:`Member` from cache for the ThreadMember object.
Returns
--------
Optional[:class:`Member`]
The member or ``None`` if not found.
"""
return self.thread.guild.get_member(self.id)

114
discord/types/activity.py Normal file
View File

@ -0,0 +1,114 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import List, Literal, Optional, TypedDict
from .user import PartialUser
from .snowflake import Snowflake
StatusType = Literal["idle", "dnd", "online", "offline"]
class PartialPresenceUpdate(TypedDict):
user: PartialUser
guild_id: Snowflake
status: StatusType
activities: List[Activity]
client_status: ClientStatus
class ClientStatus(TypedDict, total=False):
desktop: str
mobile: str
web: str
class ActivityTimestamps(TypedDict, total=False):
start: int
end: int
class ActivityParty(TypedDict, total=False):
id: str
size: List[int]
class ActivityAssets(TypedDict, total=False):
large_image: str
large_text: str
small_image: str
small_text: str
class ActivitySecrets(TypedDict, total=False):
join: str
spectate: str
match: str
class _ActivityEmojiOptional(TypedDict, total=False):
id: Snowflake
animated: bool
class ActivityEmoji(_ActivityEmojiOptional):
name: str
class ActivityButton(TypedDict):
label: str
url: str
class _SendableActivityOptional(TypedDict, total=False):
url: Optional[str]
ActivityType = Literal[0, 1, 2, 4, 5]
class SendableActivity(_SendableActivityOptional):
name: str
type: ActivityType
class _BaseActivity(SendableActivity):
created_at: int
class Activity(_BaseActivity, total=False):
state: Optional[str]
details: Optional[str]
timestamps: ActivityTimestamps
assets: ActivityAssets
party: ActivityParty
application_id: Snowflake
flags: int
emoji: Optional[ActivityEmoji]
secrets: ActivitySecrets
session_id: Optional[str]
instance: bool
buttons: List[ActivityButton]

72
discord/types/appinfo.py Normal file
View File

@ -0,0 +1,72 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import TypedDict, List, Optional
from .user import User
from .team import Team
from .snowflake import Snowflake
class BaseAppInfo(TypedDict):
id: Snowflake
name: str
verify_key: str
icon: Optional[str]
summary: str
description: str
class _AppInfoOptional(TypedDict, total=False):
team: Team
guild_id: Snowflake
primary_sku_id: Snowflake
slug: str
terms_of_service_url: str
privacy_policy_url: str
hook: bool
max_participants: int
class AppInfo(BaseAppInfo, _AppInfoOptional):
rpc_origins: List[str]
owner: User
bot_public: bool
bot_require_code_grant: bool
class _PartialAppInfoOptional(TypedDict, total=False):
rpc_origins: List[str]
cover_image: str
hook: bool
terms_of_service_url: str
privacy_policy_url: str
max_participants: int
flags: int
class PartialAppInfo(_PartialAppInfoOptional, BaseAppInfo):
pass

266
discord/types/audit_log.py Normal file
View File

@ -0,0 +1,266 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import List, Literal, Optional, TypedDict, Union
from .webhook import Webhook
from .guild import MFALevel, VerificationLevel, ExplicitContentFilterLevel, DefaultMessageNotificationLevel
from .integration import IntegrationExpireBehavior, PartialIntegration
from .user import User
from .snowflake import Snowflake
from .role import Role
from .channel import ChannelType, VideoQualityMode, PermissionOverwrite
from .threads import Thread
AuditLogEvent = Literal[
1,
10,
11,
12,
13,
14,
15,
20,
21,
22,
23,
24,
25,
26,
27,
28,
30,
31,
32,
40,
41,
42,
50,
51,
52,
60,
61,
62,
72,
73,
74,
75,
80,
81,
82,
83,
84,
85,
90,
91,
92,
110,
111,
112,
]
class _AuditLogChange_Str(TypedDict):
key: Literal[
"name",
"description",
"preferred_locale",
"vanity_url_code",
"topic",
"code",
"allow",
"deny",
"permissions",
"tags",
]
new_value: str
old_value: str
class _AuditLogChange_AssetHash(TypedDict):
key: Literal["icon_hash", "splash_hash", "discovery_splash_hash", "banner_hash", "avatar_hash", "asset"]
new_value: str
old_value: str
class _AuditLogChange_Snowflake(TypedDict):
key: Literal[
"id",
"owner_id",
"afk_channel_id",
"rules_channel_id",
"public_updates_channel_id",
"widget_channel_id",
"system_channel_id",
"application_id",
"channel_id",
"inviter_id",
"guild_id",
]
new_value: Snowflake
old_value: Snowflake
class _AuditLogChange_Bool(TypedDict):
key: Literal[
"widget_enabled",
"nsfw",
"hoist",
"mentionable",
"temporary",
"deaf",
"mute",
"nick",
"enabled_emoticons",
"region",
"rtc_region",
"available",
"archived",
"locked",
]
new_value: bool
old_value: bool
class _AuditLogChange_Int(TypedDict):
key: Literal[
"afk_timeout",
"prune_delete_days",
"position",
"bitrate",
"rate_limit_per_user",
"color",
"max_uses",
"max_age",
"user_limit",
"auto_archive_duration",
"default_auto_archive_duration",
]
new_value: int
old_value: int
class _AuditLogChange_ListRole(TypedDict):
key: Literal["$add", "$remove"]
new_value: List[Role]
old_value: List[Role]
class _AuditLogChange_MFALevel(TypedDict):
key: Literal["mfa_level"]
new_value: MFALevel
old_value: MFALevel
class _AuditLogChange_VerificationLevel(TypedDict):
key: Literal["verification_level"]
new_value: VerificationLevel
old_value: VerificationLevel
class _AuditLogChange_ExplicitContentFilter(TypedDict):
key: Literal["explicit_content_filter"]
new_value: ExplicitContentFilterLevel
old_value: ExplicitContentFilterLevel
class _AuditLogChange_DefaultMessageNotificationLevel(TypedDict):
key: Literal["default_message_notifications"]
new_value: DefaultMessageNotificationLevel
old_value: DefaultMessageNotificationLevel
class _AuditLogChange_ChannelType(TypedDict):
key: Literal["type"]
new_value: ChannelType
old_value: ChannelType
class _AuditLogChange_IntegrationExpireBehaviour(TypedDict):
key: Literal["expire_behavior"]
new_value: IntegrationExpireBehavior
old_value: IntegrationExpireBehavior
class _AuditLogChange_VideoQualityMode(TypedDict):
key: Literal["video_quality_mode"]
new_value: VideoQualityMode
old_value: VideoQualityMode
class _AuditLogChange_Overwrites(TypedDict):
key: Literal["permission_overwrites"]
new_value: List[PermissionOverwrite]
old_value: List[PermissionOverwrite]
AuditLogChange = Union[
_AuditLogChange_Str,
_AuditLogChange_AssetHash,
_AuditLogChange_Snowflake,
_AuditLogChange_Int,
_AuditLogChange_Bool,
_AuditLogChange_ListRole,
_AuditLogChange_MFALevel,
_AuditLogChange_VerificationLevel,
_AuditLogChange_ExplicitContentFilter,
_AuditLogChange_DefaultMessageNotificationLevel,
_AuditLogChange_ChannelType,
_AuditLogChange_IntegrationExpireBehaviour,
_AuditLogChange_VideoQualityMode,
_AuditLogChange_Overwrites,
]
class AuditEntryInfo(TypedDict):
delete_member_days: str
members_removed: str
channel_id: Snowflake
message_id: Snowflake
count: str
id: Snowflake
type: Literal["0", "1"]
role_name: str
class _AuditLogEntryOptional(TypedDict, total=False):
changes: List[AuditLogChange]
options: AuditEntryInfo
reason: str
class AuditLogEntry(_AuditLogEntryOptional):
target_id: Optional[str]
user_id: Optional[Snowflake]
id: Snowflake
action_type: AuditLogEvent
class AuditLog(TypedDict):
webhooks: List[Webhook]
users: List[User]
audit_log_entries: List[AuditLogEntry]
integrations: List[PartialIntegration]
threads: List[Thread]

View File

@ -22,58 +22,31 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from typing import List, Literal, Optional, TypedDict, Union
from .user import PartialUser
from .snowflake import Snowflake
from typing import List, Literal, Optional, TypedDict
from .threads import ThreadMetadata, ThreadMember, ThreadArchiveDuration
OverwriteType = Literal[0, 1]
class PermissionOverwrite(TypedDict):
id: Snowflake
type: Literal[0, 1]
type: OverwriteType
allow: str
deny: str
ChannelType = Literal[0, 1, 2, 3, 4, 5, 6, 13]
ChannelType = Literal[0, 1, 2, 3, 4, 5, 6, 10, 11, 12, 13]
class PartialChannel(TypedDict):
id: str
type: ChannelType
class _BaseChannel(TypedDict):
id: Snowflake
name: str
class _TextChannelOptional(PartialChannel, total=False):
topic: str
last_message_id: Optional[Snowflake]
last_pin_timestamp: int
rate_limit_per_user: int
class _VoiceChannelOptional(PartialChannel, total=False):
rtc_region: Optional[str]
bitrate: int
user_limit: int
class _CategoryChannelOptional(PartialChannel, total=False):
...
class _StoreChannelOptional(PartialChannel, total=False):
...
class _StageChannelOptional(PartialChannel, total=False):
rtc_region: Optional[str]
bitrate: int
user_limit: int
topic: str
class GuildChannel(
_TextChannelOptional, _VoiceChannelOptional, _CategoryChannelOptional, _StoreChannelOptional, _StageChannelOptional
):
class _BaseGuildChannel(_BaseChannel):
guild_id: Snowflake
position: int
permission_overwrites: List[PermissionOverwrite]
@ -81,11 +54,104 @@ class GuildChannel(
parent_id: Optional[Snowflake]
class DMChannel(PartialChannel):
class PartialChannel(_BaseChannel):
type: ChannelType
class _TextChannelOptional(TypedDict, total=False):
topic: str
last_message_id: Optional[Snowflake]
last_pin_timestamp: str
rate_limit_per_user: int
default_auto_archive_duration: ThreadArchiveDuration
class TextChannel(_BaseGuildChannel, _TextChannelOptional):
type: Literal[0]
class NewsChannel(_BaseGuildChannel, _TextChannelOptional):
type: Literal[5]
VideoQualityMode = Literal[1, 2]
class _VoiceChannelOptional(TypedDict, total=False):
rtc_region: Optional[str]
video_quality_mode: VideoQualityMode
class VoiceChannel(_BaseGuildChannel, _VoiceChannelOptional):
type: Literal[2]
bitrate: int
user_limit: int
class CategoryChannel(_BaseGuildChannel):
type: Literal[4]
class StoreChannel(_BaseGuildChannel):
type: Literal[6]
class _StageChannelOptional(TypedDict, total=False):
rtc_region: Optional[str]
topic: str
class StageChannel(_BaseGuildChannel, _StageChannelOptional):
type: Literal[13]
bitrate: int
user_limit: int
class _ThreadChannelOptional(TypedDict, total=False):
member: ThreadMember
owner_id: Snowflake
rate_limit_per_user: int
last_message_id: Optional[Snowflake]
last_pin_timestamp: str
class ThreadChannel(_BaseChannel, _ThreadChannelOptional):
type: Literal[10, 11, 12]
guild_id: Snowflake
parent_id: Snowflake
owner_id: Snowflake
nsfw: bool
last_message_id: Optional[Snowflake]
rate_limit_per_user: int
message_count: int
member_count: int
thread_metadata: ThreadMetadata
GuildChannel = Union[TextChannel, NewsChannel, VoiceChannel, CategoryChannel, StoreChannel, StageChannel, ThreadChannel]
class DMChannel(_BaseChannel):
type: Literal[1]
last_message_id: Optional[Snowflake]
recipients: List[PartialUser]
class GroupDMChannel(DMChannel):
class GroupDMChannel(_BaseChannel):
type: Literal[3]
icon: Optional[str]
owner_id: Snowflake
Channel = Union[GuildChannel, DMChannel, GroupDMChannel]
PrivacyLevel = Literal[1, 2]
class StageInstance(TypedDict):
id: Snowflake
guild_id: Snowflake
channel_id: Snowflake
topic: str
privacy_level: PrivacyLevel
discoverable_disabled: bool

View File

@ -0,0 +1,76 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import List, Literal, TypedDict, Union
from .emoji import PartialEmoji
ComponentType = Literal[1, 2, 3]
ButtonStyle = Literal[1, 2, 3, 4, 5]
class ActionRow(TypedDict):
type: Literal[1]
components: List[Component]
class _ButtonComponentOptional(TypedDict, total=False):
custom_id: str
url: str
disabled: bool
emoji: PartialEmoji
label: str
class ButtonComponent(_ButtonComponentOptional):
type: Literal[2]
style: ButtonStyle
class _SelectMenuOptional(TypedDict, total=False):
placeholder: str
min_values: int
max_values: int
disabled: bool
class _SelectOptionsOptional(TypedDict, total=False):
description: str
emoji: PartialEmoji
class SelectOption(_SelectOptionsOptional):
label: str
value: str
default: bool
class SelectMenu(_SelectMenuOptional):
type: Literal[3]
custom_id: str
options: List[SelectOption]
Component = Union[ActionRow, ButtonComponent, SelectMenu]

View File

@ -24,49 +24,60 @@ DEALINGS IN THE SOFTWARE.
from typing import List, Literal, TypedDict
class _EmbedFooterOptional(TypedDict, total=False):
icon_url: str
proxy_icon_url: str
class EmbedFooter(_EmbedFooterOptional):
text: str
class _EmbedFieldOptional(TypedDict, total=False):
inline: bool
class EmbedField(_EmbedFieldOptional):
name: str
value: str
class EmbedThumbnail(TypedDict, total=False):
url: str
proxy_url: str
height: int
width: int
class EmbedVideo(TypedDict, total=False):
url: str
proxy_url: str
height: int
width: int
class EmbedImage(TypedDict, total=False):
url: str
proxy_url: str
height: int
width: int
class EmbedProvider(TypedDict, total=False):
name: str
url: str
class EmbedAuthor(TypedDict, total=False):
name: str
url: str
icon_url: str
proxy_icon_url: str
EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link']
EmbedType = Literal["rich", "image", "video", "gifv", "article", "link"]
class Embed(TypedDict, total=False):
title: str

46
discord/types/emoji.py Normal file
View File

@ -0,0 +1,46 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 typing import Optional, TypedDict
from .snowflake import Snowflake, SnowflakeList
from .user import User
class PartialEmoji(TypedDict):
id: Optional[Snowflake]
name: Optional[str]
class Emoji(PartialEmoji, total=False):
roles: SnowflakeList
user: User
require_colons: bool
managed: bool
animated: bool
available: bool
class EditEmoji(TypedDict):
name: str
roles: Optional[SnowflakeList]

41
discord/types/gateway.py Normal file
View File

@ -0,0 +1,41 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 typing import TypedDict
class SessionStartLimit(TypedDict):
total: int
remaining: int
reset_after: int
max_concurrency: int
class Gateway(TypedDict):
url: str
class GatewayBot(Gateway):
shards: int
session_start_limit: SessionStartLimit

168
discord/types/guild.py Normal file
View File

@ -0,0 +1,168 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 typing import List, Literal, Optional, TypedDict
from .snowflake import Snowflake
from .channel import GuildChannel
from .voice import GuildVoiceState
from .welcome_screen import WelcomeScreen
from .activity import PartialPresenceUpdate
from .role import Role
from .member import Member
from .emoji import Emoji
from .user import User
from .threads import Thread
class Ban(TypedDict):
reason: Optional[str]
user: User
class _UnavailableGuildOptional(TypedDict, total=False):
unavailable: bool
class UnavailableGuild(_UnavailableGuildOptional):
id: Snowflake
class _GuildOptional(TypedDict, total=False):
icon_hash: Optional[str]
owner: bool
permissions: str
widget_enabled: bool
widget_channel_id: Optional[Snowflake]
joined_at: Optional[str]
large: bool
member_count: int
voice_states: List[GuildVoiceState]
members: List[Member]
channels: List[GuildChannel]
presences: List[PartialPresenceUpdate]
threads: List[Thread]
max_presences: Optional[int]
max_members: int
premium_subscription_count: int
max_video_channel_users: int
DefaultMessageNotificationLevel = Literal[0, 1]
ExplicitContentFilterLevel = Literal[0, 1, 2]
MFALevel = Literal[0, 1]
VerificationLevel = Literal[0, 1, 2, 3, 4]
NSFWLevel = Literal[0, 1, 2, 3]
PremiumTier = Literal[0, 1, 2, 3]
GuildFeature = Literal[
"ANIMATED_ICON",
"BANNER",
"COMMERCE",
"COMMUNITY",
"DISCOVERABLE",
"FEATURABLE",
"INVITE_SPLASH",
"MEMBER_VERIFICATION_GATE_ENABLED",
"MONETIZATION_ENABLED",
"MORE_EMOJI",
"MORE_STICKERS",
"NEWS",
"PARTNERED",
"PREVIEW_ENABLED",
"PRIVATE_THREADS",
"SEVEN_DAY_THREAD_ARCHIVE",
"THREE_DAY_THREAD_ARCHIVE",
"TICKETED_EVENTS_ENABLED",
"VANITY_URL",
"VERIFIED",
"VIP_REGIONS",
"WELCOME_SCREEN_ENABLED",
]
class _BaseGuildPreview(UnavailableGuild):
name: str
icon: Optional[str]
splash: Optional[str]
discovery_splash: Optional[str]
emojis: List[Emoji]
features: List[GuildFeature]
description: Optional[str]
class _GuildPreviewUnique(TypedDict):
approximate_member_count: int
approximate_presence_count: int
class GuildPreview(_BaseGuildPreview, _GuildPreviewUnique):
...
class Guild(_BaseGuildPreview, _GuildOptional):
owner_id: Snowflake
region: str
afk_channel_id: Optional[Snowflake]
afk_timeout: int
verification_level: VerificationLevel
default_message_notifications: DefaultMessageNotificationLevel
explicit_content_filter: ExplicitContentFilterLevel
roles: List[Role]
mfa_level: MFALevel
nsfw_level: NSFWLevel
application_id: Optional[Snowflake]
system_channel_id: Optional[Snowflake]
system_channel_flags: int
rules_channel_id: Optional[Snowflake]
vanity_url_code: Optional[str]
banner: Optional[str]
premium_tier: PremiumTier
preferred_locale: str
public_updates_channel_id: Optional[Snowflake]
class InviteGuild(Guild, total=False):
welcome_screen: WelcomeScreen
class GuildWithCounts(Guild, _GuildPreviewUnique):
...
class GuildPrune(TypedDict):
pruned: Optional[int]
class ChannelPositionUpdate(TypedDict):
id: Snowflake
position: Optional[int]
lock_permissions: Optional[bool]
parent_id: Optional[Snowflake]
class _RolePositionRequired(TypedDict):
id: Snowflake
class RolePositionUpdate(_RolePositionRequired, total=False):
position: Optional[Snowflake]

View File

@ -0,0 +1,82 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import Literal, Optional, TypedDict, Union
from .snowflake import Snowflake
from .user import User
class _IntegrationApplicationOptional(TypedDict, total=False):
bot: User
class IntegrationApplication(_IntegrationApplicationOptional):
id: Snowflake
name: str
icon: Optional[str]
description: str
summary: str
class IntegrationAccount(TypedDict):
id: str
name: str
IntegrationExpireBehavior = Literal[0, 1]
class PartialIntegration(TypedDict):
id: Snowflake
name: str
type: IntegrationType
account: IntegrationAccount
IntegrationType = Literal["twitch", "youtube", "discord"]
class BaseIntegration(PartialIntegration):
enabled: bool
syncing: bool
synced_at: str
user: User
expire_behavior: IntegrationExpireBehavior
expire_grace_period: int
class StreamIntegration(BaseIntegration):
role_id: Optional[Snowflake]
enable_emoticons: bool
subscriber_count: int
revoked: bool
class BotIntegration(BaseIntegration):
application: IntegrationApplication
Integration = Union[BaseIntegration, StreamIntegration, BotIntegration]

View File

@ -0,0 +1,234 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import Optional, TYPE_CHECKING, Dict, TypedDict, Union, List, Literal
from .snowflake import Snowflake
from .components import Component, ComponentType
from .embed import Embed
from .channel import ChannelType
from .member import Member
from .role import Role
from .user import User
if TYPE_CHECKING:
from .message import AllowedMentions, Message
ApplicationCommandType = Literal[1, 2, 3]
class _ApplicationCommandOptional(TypedDict, total=False):
options: List[ApplicationCommandOption]
type: ApplicationCommandType
class ApplicationCommand(_ApplicationCommandOptional):
id: Snowflake
application_id: Snowflake
name: str
description: str
class _ApplicationCommandOptionOptional(TypedDict, total=False):
choices: List[ApplicationCommandOptionChoice]
options: List[ApplicationCommandOption]
ApplicationCommandOptionType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
class ApplicationCommandOption(_ApplicationCommandOptionOptional):
type: ApplicationCommandOptionType
name: str
description: str
required: bool
class ApplicationCommandOptionChoice(TypedDict):
name: str
value: Union[str, int]
ApplicationCommandPermissionType = Literal[1, 2]
class ApplicationCommandPermissions(TypedDict):
id: Snowflake
type: ApplicationCommandPermissionType
permission: bool
class BaseGuildApplicationCommandPermissions(TypedDict):
permissions: List[ApplicationCommandPermissions]
class PartialGuildApplicationCommandPermissions(BaseGuildApplicationCommandPermissions):
id: Snowflake
class GuildApplicationCommandPermissions(PartialGuildApplicationCommandPermissions):
application_id: Snowflake
guild_id: Snowflake
InteractionType = Literal[1, 2, 3]
class _ApplicationCommandInteractionDataOption(TypedDict):
name: str
class _ApplicationCommandInteractionDataOptionSubcommand(_ApplicationCommandInteractionDataOption):
type: Literal[1, 2]
options: List[ApplicationCommandInteractionDataOption]
class _ApplicationCommandInteractionDataOptionString(_ApplicationCommandInteractionDataOption):
type: Literal[3]
value: str
class _ApplicationCommandInteractionDataOptionInteger(_ApplicationCommandInteractionDataOption):
type: Literal[4]
value: int
class _ApplicationCommandInteractionDataOptionBoolean(_ApplicationCommandInteractionDataOption):
type: Literal[5]
value: bool
class _ApplicationCommandInteractionDataOptionSnowflake(_ApplicationCommandInteractionDataOption):
type: Literal[6, 7, 8, 9]
value: Snowflake
class _ApplicationCommandInteractionDataOptionNumber(_ApplicationCommandInteractionDataOption):
type: Literal[10]
value: float
ApplicationCommandInteractionDataOption = Union[
_ApplicationCommandInteractionDataOptionString,
_ApplicationCommandInteractionDataOptionInteger,
_ApplicationCommandInteractionDataOptionSubcommand,
_ApplicationCommandInteractionDataOptionBoolean,
_ApplicationCommandInteractionDataOptionSnowflake,
_ApplicationCommandInteractionDataOptionNumber,
]
class ApplicationCommandResolvedPartialChannel(TypedDict):
id: Snowflake
type: ChannelType
permissions: str
name: str
class ApplicationCommandInteractionDataResolved(TypedDict, total=False):
users: Dict[Snowflake, User]
members: Dict[Snowflake, Member]
roles: Dict[Snowflake, Role]
channels: Dict[Snowflake, ApplicationCommandResolvedPartialChannel]
class _ApplicationCommandInteractionDataOptional(TypedDict, total=False):
options: List[ApplicationCommandInteractionDataOption]
resolved: ApplicationCommandInteractionDataResolved
target_id: Snowflake
type: ApplicationCommandType
class ApplicationCommandInteractionData(_ApplicationCommandInteractionDataOptional):
id: Snowflake
name: str
class _ComponentInteractionDataOptional(TypedDict, total=False):
values: List[str]
class ComponentInteractionData(_ComponentInteractionDataOptional):
custom_id: str
component_type: ComponentType
InteractionData = Union[ApplicationCommandInteractionData, ComponentInteractionData]
class _InteractionOptional(TypedDict, total=False):
data: InteractionData
guild_id: Snowflake
channel_id: Snowflake
member: Member
user: User
message: Message
class Interaction(_InteractionOptional):
id: Snowflake
application_id: Snowflake
type: InteractionType
token: str
version: int
class InteractionApplicationCommandCallbackData(TypedDict, total=False):
tts: bool
content: str
embeds: List[Embed]
allowed_mentions: AllowedMentions
flags: int
components: List[Component]
InteractionResponseType = Literal[1, 4, 5, 6, 7]
class _InteractionResponseOptional(TypedDict, total=False):
data: InteractionApplicationCommandCallbackData
class InteractionResponse(_InteractionResponseOptional):
type: InteractionResponseType
class MessageInteraction(TypedDict):
id: Snowflake
type: InteractionType
name: str
user: User
class _EditApplicationCommandOptional(TypedDict, total=False):
description: str
options: Optional[List[ApplicationCommandOption]]
type: ApplicationCommandType
default_permission: bool
class EditApplicationCommand(_EditApplicationCommandOptional):
name: str

99
discord/types/invite.py Normal file
View File

@ -0,0 +1,99 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import Literal, Optional, TypedDict, Union
from .snowflake import Snowflake
from .guild import InviteGuild, _GuildPreviewUnique
from .channel import PartialChannel
from .user import PartialUser
from .appinfo import PartialAppInfo
InviteTargetType = Literal[1, 2]
class _InviteOptional(TypedDict, total=False):
guild: InviteGuild
inviter: PartialUser
target_user: PartialUser
target_type: InviteTargetType
target_application: PartialAppInfo
class _InviteMetadata(TypedDict, total=False):
uses: int
max_uses: int
max_age: int
temporary: bool
created_at: str
expires_at: Optional[str]
class VanityInvite(_InviteMetadata):
code: Optional[str]
class IncompleteInvite(_InviteMetadata):
code: str
channel: PartialChannel
class Invite(IncompleteInvite, _InviteOptional):
...
class InviteWithCounts(Invite, _GuildPreviewUnique):
...
class _GatewayInviteCreateOptional(TypedDict, total=False):
guild_id: Snowflake
inviter: PartialUser
target_type: InviteTargetType
target_user: PartialUser
target_application: PartialAppInfo
class GatewayInviteCreate(_GatewayInviteCreateOptional):
channel_id: Snowflake
code: str
created_at: str
max_age: int
max_uses: int
temporary: bool
uses: bool
class _GatewayInviteDeleteOptional(TypedDict, total=False):
guild_id: Snowflake
class GatewayInviteDelete(_GatewayInviteDeleteOptional):
channel_id: Snowflake
code: str
GatewayInvite = Union[GatewayInviteCreate, GatewayInviteDelete]

63
discord/types/member.py Normal file
View File

@ -0,0 +1,63 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 typing import TypedDict
from .snowflake import SnowflakeList
from .user import User
class Nickname(TypedDict):
nick: str
class PartialMember(TypedDict):
roles: SnowflakeList
joined_at: str
deaf: str
mute: str
class Member(PartialMember, total=False):
avatar: str
user: User
nick: str
premium_since: str
pending: bool
permissions: str
class _OptionalMemberWithUser(PartialMember, total=False):
avatar: str
nick: str
premium_since: str
pending: bool
permissions: str
class MemberWithUser(_OptionalMemberWithUser):
user: User
class UserWithMember(User, total=False):
member: _OptionalMemberWithUser

139
discord/types/message.py Normal file
View File

@ -0,0 +1,139 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import List, Literal, Optional, TypedDict, Union
from .snowflake import Snowflake, SnowflakeList
from .member import Member, UserWithMember
from .user import User
from .emoji import PartialEmoji
from .embed import Embed
from .channel import ChannelType
from .components import Component
from .interactions import MessageInteraction
from .sticker import StickerItem
class ChannelMention(TypedDict):
id: Snowflake
guild_id: Snowflake
type: ChannelType
name: str
class Reaction(TypedDict):
count: int
me: bool
emoji: PartialEmoji
class _AttachmentOptional(TypedDict, total=False):
height: Optional[int]
width: Optional[int]
content_type: str
ephemeral: bool
spoiler: bool
class Attachment(_AttachmentOptional):
id: Snowflake
filename: str
size: int
url: str
proxy_url: str
MessageActivityType = Literal[1, 2, 3, 5]
class MessageActivity(TypedDict):
type: MessageActivityType
party_id: str
class _MessageApplicationOptional(TypedDict, total=False):
cover_image: str
class MessageApplication(_MessageApplicationOptional):
id: Snowflake
description: str
icon: Optional[str]
name: str
class MessageReference(TypedDict, total=False):
message_id: Snowflake
channel_id: Snowflake
guild_id: Snowflake
fail_if_not_exists: bool
class _MessageOptional(TypedDict, total=False):
guild_id: Snowflake
member: Member
mention_channels: List[ChannelMention]
reactions: List[Reaction]
nonce: Union[int, str]
webhook_id: Snowflake
activity: MessageActivity
application: MessageApplication
application_id: Snowflake
message_reference: MessageReference
flags: int
sticker_items: List[StickerItem]
referenced_message: Optional[Message]
interaction: MessageInteraction
components: List[Component]
MessageType = Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 18, 19, 20, 21]
class Message(_MessageOptional):
id: Snowflake
channel_id: Snowflake
author: User
content: str
timestamp: str
edited_timestamp: Optional[str]
tts: bool
mention_everyone: bool
mentions: List[UserWithMember]
mention_roles: SnowflakeList
attachments: List[Attachment]
embeds: List[Embed]
pinned: bool
type: MessageType
AllowedMentionType = Literal["roles", "users", "everyone"]
class AllowedMentions(TypedDict):
parse: List[AllowedMentionType]
roles: SnowflakeList
users: SnowflakeList
replied_user: bool

View File

@ -0,0 +1,98 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 typing import TypedDict, List
from .snowflake import Snowflake
from .member import Member
from .emoji import PartialEmoji
class _MessageEventOptional(TypedDict, total=False):
guild_id: Snowflake
class MessageDeleteEvent(_MessageEventOptional):
id: Snowflake
channel_id: Snowflake
class BulkMessageDeleteEvent(_MessageEventOptional):
ids: List[Snowflake]
channel_id: Snowflake
class _ReactionActionEventOptional(TypedDict, total=False):
guild_id: Snowflake
member: Member
class MessageUpdateEvent(_MessageEventOptional):
id: Snowflake
channel_id: Snowflake
class ReactionActionEvent(_ReactionActionEventOptional):
user_id: Snowflake
channel_id: Snowflake
message_id: Snowflake
emoji: PartialEmoji
class _ReactionClearEventOptional(TypedDict, total=False):
guild_id: Snowflake
class ReactionClearEvent(_ReactionClearEventOptional):
channel_id: Snowflake
message_id: Snowflake
class _ReactionClearEmojiEventOptional(TypedDict, total=False):
guild_id: Snowflake
class ReactionClearEmojiEvent(_ReactionClearEmojiEventOptional):
channel_id: int
message_id: int
emoji: PartialEmoji
class _IntegrationDeleteEventOptional(TypedDict, total=False):
application_id: Snowflake
class IntegrationDeleteEvent(_IntegrationDeleteEventOptional):
id: Snowflake
guild_id: Snowflake
class _TypingEventOptional(TypedDict, total=False):
guild_id: Snowflake
member: Member
class TypingEvent(_TypingEventOptional):
channel_id: Snowflake
user_id: Snowflake
timestamp: int

49
discord/types/role.py Normal file
View File

@ -0,0 +1,49 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import TypedDict
from .snowflake import Snowflake
class _RoleOptional(TypedDict, total=False):
tags: RoleTags
class Role(_RoleOptional):
id: Snowflake
name: str
color: int
hoist: bool
position: int
permissions: str
managed: bool
mentionable: bool
class RoleTags(TypedDict, total=False):
bot_id: Snowflake
integration_id: Snowflake
premium_subscriber: None

View File

@ -22,7 +22,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from typing import List
from typing import List, Union
Snowflake = str
Snowflake = Union[str, int]
SnowflakeList = List[Snowflake]

93
discord/types/sticker.py Normal file
View File

@ -0,0 +1,93 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import List, Literal, TypedDict, Union
from .snowflake import Snowflake
from .user import User
StickerFormatType = Literal[1, 2, 3]
class StickerItem(TypedDict):
id: Snowflake
name: str
format_type: StickerFormatType
class BaseSticker(TypedDict):
id: Snowflake
name: str
description: str
tags: str
format_type: StickerFormatType
class StandardSticker(BaseSticker):
type: Literal[1]
sort_value: int
pack_id: Snowflake
class _GuildStickerOptional(TypedDict, total=False):
user: User
class GuildSticker(BaseSticker, _GuildStickerOptional):
type: Literal[2]
available: bool
guild_id: Snowflake
Sticker = Union[BaseSticker, StandardSticker, GuildSticker]
class StickerPack(TypedDict):
id: Snowflake
stickers: List[StandardSticker]
name: str
sku_id: Snowflake
cover_sticker_id: Snowflake
description: str
banner_asset_id: Snowflake
class _CreateGuildStickerOptional(TypedDict, total=False):
description: str
class CreateGuildSticker(_CreateGuildStickerOptional):
name: str
tags: str
class EditGuildSticker(TypedDict, total=False):
name: str
tags: str
description: str
class ListPremiumStickerPacks(TypedDict):
sticker_packs: List[StickerPack]

45
discord/types/team.py Normal file
View File

@ -0,0 +1,45 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import TypedDict, List, Optional
from .user import PartialUser
from .snowflake import Snowflake
class TeamMember(TypedDict):
user: PartialUser
membership_state: int
permissions: List[str]
team_id: Snowflake
class Team(TypedDict):
id: Snowflake
name: str
owner_id: Snowflake
members: List[TeamMember]
icon: Optional[str]

49
discord/types/template.py Normal file
View File

@ -0,0 +1,49 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import Optional, TypedDict
from .snowflake import Snowflake
from .user import User
from .guild import Guild
class CreateTemplate(TypedDict):
name: str
icon: Optional[bytes]
class Template(TypedDict):
code: str
name: str
description: Optional[str]
usage_count: int
creator_id: Snowflake
creator: User
created_at: str
updated_at: str
source_guild_id: Snowflake
serialized_source_guild: Guild
is_dirty: Optional[bool]

75
discord/types/threads.py Normal file
View File

@ -0,0 +1,75 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import List, Literal, Optional, TypedDict
from .snowflake import Snowflake
ThreadType = Literal[10, 11, 12]
ThreadArchiveDuration = Literal[60, 1440, 4320, 10080]
class ThreadMember(TypedDict):
id: Snowflake
user_id: Snowflake
join_timestamp: str
flags: int
class _ThreadMetadataOptional(TypedDict, total=False):
archiver_id: Snowflake
locked: bool
invitable: bool
class ThreadMetadata(_ThreadMetadataOptional):
archived: bool
auto_archive_duration: ThreadArchiveDuration
archive_timestamp: str
class _ThreadOptional(TypedDict, total=False):
member: ThreadMember
last_message_id: Optional[Snowflake]
last_pin_timestamp: Optional[Snowflake]
class Thread(_ThreadOptional):
id: Snowflake
guild_id: Snowflake
parent_id: Snowflake
owner_id: Snowflake
name: str
type: ThreadType
member_count: int
message_count: int
rate_limit_per_user: int
thread_metadata: ThreadMetadata
class ThreadPaginationPayload(TypedDict):
threads: List[Thread]
members: List[ThreadMember]
has_more: bool

View File

@ -23,7 +23,7 @@ DEALINGS IN THE SOFTWARE.
"""
from .snowflake import Snowflake
from typing import Optional, TypedDict
from typing import Literal, Optional, TypedDict
class PartialUser(TypedDict):
@ -31,3 +31,18 @@ class PartialUser(TypedDict):
username: str
discriminator: str
avatar: Optional[str]
PremiumType = Literal[0, 1, 2]
class User(PartialUser, total=False):
bot: bool
system: bool
mfa_enabled: bool
local: str
verified: bool
email: Optional[str]
flags: int
premium_type: PremiumType
public_flags: int

85
discord/types/voice.py Normal file
View File

@ -0,0 +1,85 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 typing import Optional, TypedDict, List, Literal
from .snowflake import Snowflake
from .member import MemberWithUser
SupportedModes = Literal["xsalsa20_poly1305_lite", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305"]
class _PartialVoiceStateOptional(TypedDict, total=False):
member: MemberWithUser
self_stream: bool
class _VoiceState(_PartialVoiceStateOptional):
user_id: Snowflake
session_id: str
deaf: bool
mute: bool
self_deaf: bool
self_mute: bool
self_video: bool
suppress: bool
class GuildVoiceState(_VoiceState):
channel_id: Snowflake
class VoiceState(_VoiceState, total=False):
channel_id: Optional[Snowflake]
guild_id: Snowflake
class VoiceRegion(TypedDict):
id: str
name: str
vip: bool
optimal: bool
deprecated: bool
custom: bool
class VoiceServerUpdate(TypedDict):
token: str
guild_id: Snowflake
endpoint: Optional[str]
class VoiceIdentify(TypedDict):
server_id: Snowflake
user_id: Snowflake
session_id: str
token: str
class VoiceReady(TypedDict):
ssrc: int
ip: str
port: int
modes: List[SupportedModes]
heartbeat_interval: int

70
discord/types/webhook.py Normal file
View File

@ -0,0 +1,70 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import Literal, Optional, TypedDict
from .snowflake import Snowflake
from .user import User
from .channel import PartialChannel
class SourceGuild(TypedDict):
id: int
name: str
icon: str
class _WebhookOptional(TypedDict, total=False):
guild_id: Snowflake
user: User
token: str
WebhookType = Literal[1, 2, 3]
class _FollowerWebhookOptional(TypedDict, total=False):
source_channel: PartialChannel
source_guild: SourceGuild
class FollowerWebhook(_FollowerWebhookOptional):
channel_id: Snowflake
webhook_id: Snowflake
class PartialWebhook(_WebhookOptional):
id: Snowflake
type: WebhookType
class _FullWebhook(TypedDict, total=False):
name: Optional[str]
avatar: Optional[str]
channel_id: Snowflake
application_id: Optional[Snowflake]
class Webhook(PartialWebhook, _FullWebhook):
...

View File

@ -0,0 +1,40 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import List, Optional, TypedDict
from .snowflake import Snowflake
class WelcomeScreen(TypedDict):
description: str
welcome_channels: List[WelcomeScreenChannel]
class WelcomeScreenChannel(TypedDict):
channel_id: Snowflake
description: str
emoji_id: Optional[Snowflake]
emoji_name: Optional[str]

63
discord/types/widget.py Normal file
View File

@ -0,0 +1,63 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 typing import List, Optional, TypedDict
from .activity import Activity
from .snowflake import Snowflake
from .user import User
class WidgetChannel(TypedDict):
id: Snowflake
name: str
position: int
class WidgetMember(User, total=False):
nick: str
game: Activity
status: str
avatar_url: str
deaf: bool
self_deaf: bool
mute: bool
self_mute: bool
suppress: bool
class _WidgetOptional(TypedDict, total=False):
channels: List[WidgetChannel]
members: List[WidgetMember]
presence_count: int
class Widget(_WidgetOptional):
id: Snowflake
name: str
instant_invite: str
class WidgetSettings(TypedDict):
enabled: bool
channel_id: Optional[Snowflake]

15
discord/ui/__init__.py Normal file
View File

@ -0,0 +1,15 @@
"""
discord.ui
~~~~~~~~~~~
Bot UI Kit helper for the Discord API
:copyright: (c) 2015-present Rapptz
:license: MIT, see LICENSE for more details.
"""
from .view import *
from .item import *
from .button import *
from .select import *

290
discord/ui/button.py Normal file
View File

@ -0,0 +1,290 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import Any, Callable, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union
import inspect
import os
from .item import Item, ItemCallbackType
from ..enums import ButtonStyle, ComponentType
from ..partial_emoji import PartialEmoji, _EmojiTag
from ..components import Button as ButtonComponent
__all__ = (
"Button",
"button",
)
if TYPE_CHECKING:
from .view import View
from ..emoji import Emoji
B = TypeVar("B", bound="Button")
V = TypeVar("V", bound="View", covariant=True)
class Button(Item[V]):
"""Represents a UI button.
.. versionadded:: 2.0
Parameters
------------
style: :class:`discord.ButtonStyle`
The style of the button.
custom_id: Optional[:class:`str`]
The ID of the button that gets received during an interaction.
If this button is for a URL, it does not have a custom ID.
url: Optional[:class:`str`]
The URL this button sends you to. This param is automatically casted to :class:`str`.
disabled: :class:`bool`
Whether the button is disabled or not.
label: Optional[:class:`str`]
The label of the button, if any.
emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.Emoji`, :class:`str`]]
The emoji of the button, if available.
row: Optional[:class:`int`]
The relative row this button belongs to. A Discord component can only have 5
rows. By default, items are arranged automatically into those 5 rows. If you'd
like to control the relative positioning of the row then passing an index is advised.
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
"""
__item_repr_attributes__: Tuple[str, ...] = (
"style",
"url",
"disabled",
"label",
"emoji",
"row",
)
def __init__(
self,
*,
style: ButtonStyle = ButtonStyle.secondary,
label: Optional[str] = None,
disabled: bool = False,
custom_id: Optional[str] = None,
url: Optional[Any] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
row: Optional[int] = None,
):
super().__init__()
if custom_id is not None and url is not None:
raise TypeError("cannot mix both url and custom_id with Button")
self._provided_custom_id = custom_id is not None
if url is None and custom_id is None:
custom_id = os.urandom(16).hex()
if url is not None:
style = ButtonStyle.link
if emoji is not None:
if isinstance(emoji, str):
emoji = PartialEmoji.from_str(emoji)
elif isinstance(emoji, _EmojiTag):
emoji = emoji._to_partial()
else:
raise TypeError(f"expected emoji to be str, Emoji, or PartialEmoji not {emoji.__class__}")
self._underlying = ButtonComponent._raw_construct(
type=ComponentType.button,
custom_id=custom_id,
url=str(url) if url else None,
disabled=disabled,
label=label,
style=style,
emoji=emoji,
)
self.row = row
@property
def style(self) -> ButtonStyle:
""":class:`discord.ButtonStyle`: The style of the button."""
return self._underlying.style
@style.setter
def style(self, value: ButtonStyle):
self._underlying.style = value
@property
def custom_id(self) -> Optional[str]:
"""Optional[:class:`str`]: The ID of the button that gets received during an interaction.
If this button is for a URL, it does not have a custom ID.
"""
return self._underlying.custom_id
@custom_id.setter
def custom_id(self, value: Optional[str]):
if value is not None and not isinstance(value, str):
raise TypeError("custom_id must be None or str")
self._underlying.custom_id = value
@property
def url(self) -> Optional[str]:
"""Optional[:class:`str`]: The URL this button sends you to."""
return self._underlying.url
@url.setter
def url(self, value: Optional[str]):
if value is not None and not isinstance(value, str):
raise TypeError("url must be None or str")
self._underlying.url = value
@property
def disabled(self) -> bool:
""":class:`bool`: Whether the button is disabled or not."""
return self._underlying.disabled
@disabled.setter
def disabled(self, value: bool):
self._underlying.disabled = bool(value)
@property
def label(self) -> Optional[str]:
"""Optional[:class:`str`]: The label of the button, if available."""
return self._underlying.label
@label.setter
def label(self, value: Optional[str]):
self._underlying.label = str(value) if value is not None else value
@property
def emoji(self) -> Optional[PartialEmoji]:
"""Optional[:class:`.PartialEmoji`]: The emoji of the button, if available."""
return self._underlying.emoji
@emoji.setter
def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]): # type: ignore
if value is not None:
if isinstance(value, str):
self._underlying.emoji = PartialEmoji.from_str(value)
elif isinstance(value, _EmojiTag):
self._underlying.emoji = value._to_partial()
else:
raise TypeError(f"expected str, Emoji, or PartialEmoji, received {value.__class__} instead")
else:
self._underlying.emoji = None
@classmethod
def from_component(cls: Type[B], button: ButtonComponent) -> B:
return cls(
style=button.style,
label=button.label,
disabled=button.disabled,
custom_id=button.custom_id,
url=button.url,
emoji=button.emoji,
row=None,
)
@property
def type(self) -> ComponentType:
return self._underlying.type
def to_component_dict(self):
return self._underlying.to_dict()
def is_dispatchable(self) -> bool:
return self.custom_id is not None
def is_persistent(self) -> bool:
if self.style is ButtonStyle.link:
return self.url is not None
return super().is_persistent()
def refresh_component(self, button: ButtonComponent) -> None:
self._underlying = button
def button(
*,
label: Optional[str] = None,
custom_id: Optional[str] = None,
disabled: bool = False,
style: ButtonStyle = ButtonStyle.secondary,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
row: Optional[int] = None,
) -> Callable[[ItemCallbackType], ItemCallbackType]:
"""A decorator that attaches a button to a component.
The function being decorated should have three parameters, ``self`` representing
the :class:`discord.ui.View`, the :class:`discord.ui.Button` being pressed and
the :class:`discord.Interaction` you receive.
.. note::
Buttons with a URL cannot be created with this function.
Consider creating a :class:`Button` manually instead.
This is because buttons with a URL do not have a callback
associated with them since Discord does not do any processing
with it.
Parameters
------------
label: Optional[:class:`str`]
The label of the button, if any.
custom_id: Optional[:class:`str`]
The ID of the button that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts.
style: :class:`.ButtonStyle`
The style of the button. Defaults to :attr:`.ButtonStyle.grey`.
disabled: :class:`bool`
Whether the button is disabled or not. Defaults to ``False``.
emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]]
The emoji of the button. This can be in string form or a :class:`.PartialEmoji`
or a full :class:`.Emoji`.
row: Optional[:class:`int`]
The relative row this button belongs to. A Discord component can only have 5
rows. By default, items are arranged automatically into those 5 rows. If you'd
like to control the relative positioning of the row then passing an index is advised.
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
"""
def decorator(func: ItemCallbackType) -> ItemCallbackType:
if not inspect.iscoroutinefunction(func):
raise TypeError("button function must be a coroutine function")
func.__discord_ui_model_type__ = Button
func.__discord_ui_model_kwargs__ = {
"style": style,
"custom_id": custom_id,
"url": None,
"disabled": disabled,
"label": label,
"emoji": emoji,
"row": row,
}
return func
return decorator

129
discord/ui/item.py Normal file
View File

@ -0,0 +1,129 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import Any, Callable, Coroutine, Dict, Generic, Optional, TYPE_CHECKING, Tuple, Type, TypeVar
from ..interactions import Interaction
__all__ = ("Item",)
if TYPE_CHECKING:
from ..enums import ComponentType
from .view import View
from ..components import Component
I = TypeVar("I", bound="Item")
V = TypeVar("V", bound="View", covariant=True)
ItemCallbackType = Callable[[Any, I, Interaction], Coroutine[Any, Any, Any]]
class Item(Generic[V]):
"""Represents the base UI item that all UI components inherit from.
The current UI items supported are:
- :class:`discord.ui.Button`
- :class:`discord.ui.Select`
.. versionadded:: 2.0
"""
__item_repr_attributes__: Tuple[str, ...] = ("row",)
def __init__(self):
self._view: Optional[V] = None
self._row: Optional[int] = None
self._rendered_row: Optional[int] = None
# This works mostly well but there is a gotcha with
# the interaction with from_component, since that technically provides
# a custom_id most dispatchable items would get this set to True even though
# it might not be provided by the library user. However, this edge case doesn't
# actually affect the intended purpose of this check because from_component is
# only called upon edit and we're mainly interested during initial creation time.
self._provided_custom_id: bool = False
def to_component_dict(self) -> Dict[str, Any]:
raise NotImplementedError
def refresh_component(self, component: Component) -> None:
return None
def refresh_state(self, interaction: Interaction) -> None:
return None
@classmethod
def from_component(cls: Type[I], component: Component) -> I:
return cls()
@property
def type(self) -> ComponentType:
raise NotImplementedError
def is_dispatchable(self) -> bool:
return False
def is_persistent(self) -> bool:
return self._provided_custom_id
def __repr__(self) -> str:
attrs = " ".join(f"{key}={getattr(self, key)!r}" for key in self.__item_repr_attributes__)
return f"<{self.__class__.__name__} {attrs}>"
@property
def row(self) -> Optional[int]:
return self._row
@row.setter
def row(self, value: Optional[int]):
if value is None:
self._row = None
elif 5 > value >= 0:
self._row = value
else:
raise ValueError("row cannot be negative or greater than or equal to 5")
@property
def width(self) -> int:
return 1
@property
def view(self) -> Optional[V]:
"""Optional[:class:`View`]: The underlying view for this item."""
return self._view
async def callback(self, interaction: Interaction):
"""|coro|
The callback associated with this UI item.
This can be overriden by subclasses.
Parameters
-----------
interaction: :class:`.Interaction`
The interaction that triggered this UI item.
"""
pass

356
discord/ui/select.py Normal file
View File

@ -0,0 +1,356 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present 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 __future__ import annotations
from typing import List, Optional, TYPE_CHECKING, Tuple, TypeVar, Type, Callable, Union
import inspect
import os
from .item import Item, ItemCallbackType
from ..enums import ComponentType
from ..partial_emoji import PartialEmoji
from ..emoji import Emoji
from ..interactions import Interaction
from ..utils import MISSING
from ..components import (
SelectOption,
SelectMenu,
)
__all__ = (
"Select",
"select",
)
if TYPE_CHECKING:
from .view import View
from ..types.components import SelectMenu as SelectMenuPayload
from ..types.interactions import (
ComponentInteractionData,
)
S = TypeVar("S", bound="Select")
V = TypeVar("V", bound="View", covariant=True)
class Select(Item[V]):
"""Represents a UI select menu.
This is usually represented as a drop down menu.
In order to get the selected items that the user has chosen, use :attr:`Select.values`.
.. versionadded:: 2.0
Parameters
------------
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
If not given then one is generated for you.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 0 and 25.
max_values: :class:`int`
The maximum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
options: List[:class:`discord.SelectOption`]
A list of options that can be selected in this menu.
disabled: :class:`bool`
Whether the select is disabled or not.
row: Optional[:class:`int`]
The relative row this select menu belongs to. A Discord component can only have 5
rows. By default, items are arranged automatically into those 5 rows. If you'd
like to control the relative positioning of the row then passing an index is advised.
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
"""
__item_repr_attributes__: Tuple[str, ...] = (
"placeholder",
"min_values",
"max_values",
"options",
"disabled",
)
def __init__(
self,
*,
custom_id: str = MISSING,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
options: List[SelectOption] = MISSING,
disabled: bool = False,
row: Optional[int] = None,
) -> None:
super().__init__()
self._selected_values: List[str] = []
self._provided_custom_id = custom_id is not MISSING
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
options = [] if options is MISSING else options
self._underlying = SelectMenu._raw_construct(
custom_id=custom_id,
type=ComponentType.select,
placeholder=placeholder,
min_values=min_values,
max_values=max_values,
options=options,
disabled=disabled,
)
self.row = row
@property
def custom_id(self) -> str:
""":class:`str`: The ID of the select menu that gets received during an interaction."""
return self._underlying.custom_id
@custom_id.setter
def custom_id(self, value: str):
if not isinstance(value, str):
raise TypeError("custom_id must be None or str")
self._underlying.custom_id = value
@property
def placeholder(self) -> Optional[str]:
"""Optional[:class:`str`]: The placeholder text that is shown if nothing is selected, if any."""
return self._underlying.placeholder
@placeholder.setter
def placeholder(self, value: Optional[str]):
if value is not None and not isinstance(value, str):
raise TypeError("placeholder must be None or str")
self._underlying.placeholder = value
@property
def min_values(self) -> int:
""":class:`int`: The minimum number of items that must be chosen for this select menu."""
return self._underlying.min_values
@min_values.setter
def min_values(self, value: int):
self._underlying.min_values = int(value)
@property
def max_values(self) -> int:
""":class:`int`: The maximum number of items that must be chosen for this select menu."""
return self._underlying.max_values
@max_values.setter
def max_values(self, value: int):
self._underlying.max_values = int(value)
@property
def options(self) -> List[SelectOption]:
"""List[:class:`discord.SelectOption`]: A list of options that can be selected in this menu."""
return self._underlying.options
@options.setter
def options(self, value: List[SelectOption]):
if not isinstance(value, list):
raise TypeError("options must be a list of SelectOption")
if not all(isinstance(obj, SelectOption) for obj in value):
raise TypeError("all list items must subclass SelectOption")
self._underlying.options = value
def add_option(
self,
*,
label: str,
value: str = MISSING,
description: Optional[str] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
default: bool = False,
):
"""Adds an option to the select menu.
To append a pre-existing :class:`discord.SelectOption` use the
:meth:`append_option` method instead.
Parameters
-----------
label: :class:`str`
The label of the option. This is displayed to users.
Can only be up to 100 characters.
value: :class:`str`
The value of the option. This is not displayed to users.
If not given, defaults to the label. Can only be up to 100 characters.
description: Optional[:class:`str`]
An additional description of the option, if any.
Can only be up to 100 characters.
emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]]
The emoji of the option, if available. This can either be a string representing
the custom or unicode emoji or an instance of :class:`.PartialEmoji` or :class:`.Emoji`.
default: :class:`bool`
Whether this option is selected by default.
Raises
-------
ValueError
The number of options exceeds 25.
"""
option = SelectOption(
label=label,
value=value,
description=description,
emoji=emoji,
default=default,
)
self.append_option(option)
def append_option(self, option: SelectOption):
"""Appends an option to the select menu.
Parameters
-----------
option: :class:`discord.SelectOption`
The option to append to the select menu.
Raises
-------
ValueError
The number of options exceeds 25.
"""
if len(self._underlying.options) > 25:
raise ValueError("maximum number of options already provided")
self._underlying.options.append(option)
@property
def disabled(self) -> bool:
""":class:`bool`: Whether the select is disabled or not."""
return self._underlying.disabled
@disabled.setter
def disabled(self, value: bool):
self._underlying.disabled = bool(value)
@property
def values(self) -> List[str]:
"""List[:class:`str`]: A list of values that have been selected by the user."""
return self._selected_values
@property
def width(self) -> int:
return 5
def to_component_dict(self) -> SelectMenuPayload:
return self._underlying.to_dict()
def refresh_component(self, component: SelectMenu) -> None:
self._underlying = component
def refresh_state(self, interaction: Interaction) -> None:
data: ComponentInteractionData = interaction.data # type: ignore
self._selected_values = data.get("values", [])
@classmethod
def from_component(cls: Type[S], component: SelectMenu) -> S:
return cls(
custom_id=component.custom_id,
placeholder=component.placeholder,
min_values=component.min_values,
max_values=component.max_values,
options=component.options,
disabled=component.disabled,
row=None,
)
@property
def type(self) -> ComponentType:
return self._underlying.type
def is_dispatchable(self) -> bool:
return True
def select(
*,
placeholder: Optional[str] = None,
custom_id: str = MISSING,
min_values: int = 1,
max_values: int = 1,
options: List[SelectOption] = MISSING,
disabled: bool = False,
row: Optional[int] = None,
) -> Callable[[ItemCallbackType], ItemCallbackType]:
"""A decorator that attaches a select menu to a component.
The function being decorated should have three parameters, ``self`` representing
the :class:`discord.ui.View`, the :class:`discord.ui.Select` being pressed and
the :class:`discord.Interaction` you receive.
In order to get the selected items that the user has chosen within the callback
use :attr:`Select.values`.
Parameters
------------
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts.
row: Optional[:class:`int`]
The relative row this select menu belongs to. A Discord component can only have 5
rows. By default, items are arranged automatically into those 5 rows. If you'd
like to control the relative positioning of the row then passing an index is advised.
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 0 and 25.
max_values: :class:`int`
The maximum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
options: List[:class:`discord.SelectOption`]
A list of options that can be selected in this menu.
disabled: :class:`bool`
Whether the select is disabled or not. Defaults to ``False``.
"""
def decorator(func: ItemCallbackType) -> ItemCallbackType:
if not inspect.iscoroutinefunction(func):
raise TypeError("select function must be a coroutine function")
func.__discord_ui_model_type__ = Select
func.__discord_ui_model_kwargs__ = {
"placeholder": placeholder,
"custom_id": custom_id,
"row": row,
"min_values": min_values,
"max_values": max_values,
"options": options,
"disabled": disabled,
}
return func
return decorator

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