356 Commits

Author SHA1 Message Date
Josh
a7c95966ac Fix issue when tasks loop used in classes. 2021-06-28 15:46:36 +10:00
Josh
faa3e84cfb Remove type-ignore related to Pyright issue. 2021-06-11 14:55:27 +10:00
Josh
3a71d3be5f Use ParamSpec in ext-tasks 2021-06-10 22:54:59 +10: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
125 changed files with 16116 additions and 4465 deletions

View File

@@ -1,7 +1,6 @@
name: Bug Report
description: Report broken or incorrect behaviour
labels: unconfirmed bug
issue_body: true
body:
- type: markdown
attributes:
@@ -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

@@ -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

@@ -59,7 +59,7 @@ To install the development version, do the following:
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:

View File

@@ -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,7 +55,10 @@ 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')

View File

@@ -64,10 +64,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()

View File

@@ -24,10 +24,9 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
import sys
import copy
import asyncio
from typing import TYPE_CHECKING, Optional, Protocol, runtime_checkable
from typing import Any, Dict, List, Mapping, Optional, TYPE_CHECKING, Protocol, Type, TypeVar, Union, overload, runtime_checkable
from .iterators import HistoryIterator
from .context_managers import Typing
@@ -50,10 +49,25 @@ __all__ = (
'Connectable',
)
T = TypeVar('T', bound=VoiceProtocol)
if TYPE_CHECKING:
from datetime import datetime
from .user import ClientUser
from .asset import Asset
from .state import ConnectionState
from .guild import Guild
from .member import Member
from .channel import CategoryChannel
from .embeds import Embed
from .message import Message, MessageReference
from .enums import InviteTarget
from .ui.view import View
SnowflakeTime = Union["Snowflake", datetime]
MISSING = utils.MISSING
class _Undefined:
@@ -61,7 +75,7 @@ class _Undefined:
return 'see-below'
_undefined = _Undefined()
_undefined: Any = _Undefined()
@runtime_checkable
@@ -79,6 +93,7 @@ class Snowflake(Protocol):
id: :class:`int`
The model's unique ID.
"""
__slots__ = ()
id: int
@@ -87,7 +102,6 @@ class Snowflake(Protocol):
""":class:`datetime.datetime`: Returns the model's creation time as an aware datetime in UTC."""
raise NotImplementedError
@runtime_checkable
class User(Snowflake, Protocol):
"""An ABC that details the common operations on a Discord user.
@@ -106,16 +120,17 @@ class User(Snowflake, Protocol):
The user's username.
discriminator: :class:`str`
The user's discriminator.
avatar: Optional[:class:`str`]
The avatar hash the user has.
avatar: :class:`~discord.Asset`
The avatar asset the user has.
bot: :class:`bool`
If the user is a bot account.
"""
__slots__ = ()
name: str
discriminator: str
avatar: Optional[str]
avatar: Asset
bot: bool
@property
@@ -145,6 +160,7 @@ class PrivateChannel(Snowflake, Protocol):
me: :class:`~discord.ClientUser`
The user presenting yourself.
"""
__slots__ = ()
me: ClientUser
@@ -153,11 +169,14 @@ class PrivateChannel(Snowflake, Protocol):
class _Overwrites:
__slots__ = ('id', 'allow', 'deny', 'type')
ROLE = 0
MEMBER = 1
def __init__(self, **kwargs):
self.id = kwargs.pop('id')
self.allow = int(kwargs.pop('allow_new', 0))
self.deny = int(kwargs.pop('deny_new', 0))
self.type = sys.intern(kwargs.pop('type'))
self.allow = int(kwargs.pop('allow', 0))
self.deny = int(kwargs.pop('deny', 0))
self.type = kwargs.pop('type')
def _asdict(self):
return {
@@ -167,8 +186,17 @@ class _Overwrites:
'type': self.type,
}
def is_role(self) -> bool:
return self.type == 0
class GuildChannel(Protocol):
def is_member(self) -> bool:
return self.type == 1
GCH = TypeVar('GCH', bound='GuildChannel')
class GuildChannel:
"""An ABC that details the common operations on a Discord guild channel.
The following implement this ABC:
@@ -176,6 +204,7 @@ class GuildChannel(Protocol):
- :class:`~discord.TextChannel`
- :class:`~discord.VoiceChannel`
- :class:`~discord.CategoryChannel`
- :class:`~discord.StageChannel`
This ABC must also implement :class:`~discord.abc.Snowflake`.
@@ -194,16 +223,38 @@ class GuildChannel(Protocol):
The position in the channel list. This is a number that starts at 0.
e.g. the top channel is position 0.
"""
__slots__ = ()
def __str__(self):
id: int
name: str
guild: Guild
type: ChannelType
_state: ConnectionState
if TYPE_CHECKING:
def __init__(self, *, state: ConnectionState, guild: Guild, data: Dict[str, Any]):
...
def __str__(self) -> str:
return self.name
@property
def _sorting_bucket(self):
def _sorting_bucket(self) -> int:
raise NotImplementedError
async def _move(self, position, parent_id=None, lock_permissions=False, *, reason):
def _update(self, guild: Guild, data: Dict[str, Any]) -> None:
raise NotImplementedError
async def _move(
self,
position: int,
parent_id: Optional[Any] = None,
lock_permissions: bool = False,
*,
reason: Optional[str],
):
if position < 0:
raise InvalidArgument('Channel position cannot be less than 0.')
@@ -256,6 +307,13 @@ class GuildChannel(Protocol):
else:
options['rtc_region'] = None if rtc_region is None else str(rtc_region)
try:
video_quality_mode = options.pop('video_quality_mode')
except KeyError:
pass
else:
options['video_quality_mode'] = int(video_quality_mode)
lock_permissions = options.pop('sync_permissions', False)
try:
@@ -279,19 +337,19 @@ class GuildChannel(Protocol):
perms = []
for target, perm in overwrites.items():
if not isinstance(perm, PermissionOverwrite):
raise InvalidArgument('Expected PermissionOverwrite received {0.__name__}'.format(type(perm)))
raise InvalidArgument(f'Expected PermissionOverwrite received {perm.__class__.__name__}')
allow, deny = perm.pair()
payload = {
'allow': allow.value,
'deny': deny.value,
'id': target.id
'id': target.id,
}
if isinstance(target, Role):
payload['type'] = 'role'
payload['type'] = _Overwrites.ROLE
else:
payload['type'] = 'member'
payload['type'] = _Overwrites.MEMBER
perms.append(payload)
options['permission_overwrites'] = perms
@@ -318,7 +376,7 @@ class GuildChannel(Protocol):
overridden_id = int(overridden.pop('id'))
self._overwrites.append(_Overwrites(id=overridden_id, **overridden))
if overridden['type'] == 'member':
if overridden['type'] == _Overwrites.MEMBER:
continue
if overridden_id == everyone_id:
@@ -335,12 +393,12 @@ class GuildChannel(Protocol):
tmp[everyone_index], tmp[0] = tmp[0], tmp[everyone_index]
@property
def changed_roles(self):
def changed_roles(self) -> List[Role]:
"""List[:class:`~discord.Role`]: Returns a list of roles that have been overridden from
their default values in the :attr:`~discord.Guild.roles` attribute."""
ret = []
g = self.guild
for overwrite in filter(lambda o: o.type == 'role', self._overwrites):
for overwrite in filter(lambda o: o.is_role(), self._overwrites):
role = g.get_role(overwrite.id)
if role is None:
continue
@@ -351,16 +409,16 @@ class GuildChannel(Protocol):
return ret
@property
def mention(self):
def mention(self) -> str:
""":class:`str`: The string that allows you to mention the channel."""
return f'<#{self.id}>'
@property
def created_at(self):
def created_at(self) -> datetime:
""":class:`datetime.datetime`: Returns the channel's creation time in UTC."""
return utils.snowflake_time(self.id)
def overwrites_for(self, obj):
def overwrites_for(self, obj: Union[Role, User]) -> PermissionOverwrite:
"""Returns the channel-specific overwrites for a member or a role.
Parameters
@@ -376,9 +434,9 @@ class GuildChannel(Protocol):
"""
if isinstance(obj, User):
predicate = lambda p: p.type == 'member'
predicate = lambda p: p.is_member()
elif isinstance(obj, Role):
predicate = lambda p: p.type == 'role'
predicate = lambda p: p.is_role()
else:
predicate = lambda p: True
@@ -391,7 +449,7 @@ class GuildChannel(Protocol):
return PermissionOverwrite()
@property
def overwrites(self):
def overwrites(self) -> Mapping[Union[Role, Member], PermissionOverwrite]:
"""Returns all of the channel's overwrites.
This is returned as a dictionary where the key contains the target which
@@ -408,10 +466,11 @@ class GuildChannel(Protocol):
allow = Permissions(ow.allow)
deny = Permissions(ow.deny)
overwrite = PermissionOverwrite.from_pair(allow, deny)
target = None
if ow.type == 'role':
if ow.is_role():
target = self.guild.get_role(ow.id)
elif ow.type == 'member':
elif ow.is_member():
target = self.guild.get_member(ow.id)
# TODO: There is potential data loss here in the non-chunked
@@ -424,7 +483,7 @@ class GuildChannel(Protocol):
return ret
@property
def category(self):
def category(self) -> Optional[CategoryChannel]:
"""Optional[:class:`~discord.CategoryChannel`]: The category this channel belongs to.
If there is no category then this is ``None``.
@@ -432,7 +491,7 @@ class GuildChannel(Protocol):
return self.guild.get_channel(self.category_id)
@property
def permissions_synced(self):
def permissions_synced(self) -> bool:
""":class:`bool`: Whether or not the permissions for this channel are synced with the
category it belongs to.
@@ -443,8 +502,9 @@ class GuildChannel(Protocol):
category = self.guild.get_channel(self.category_id)
return bool(category and category.overwrites == self.overwrites)
def permissions_for(self, member):
"""Handles permission resolution for the current :class:`~discord.Member`.
def permissions_for(self, obj: Union[Member, Role], /) -> Permissions:
"""Handles permission resolution for the :class:`~discord.Member`
or :class:`~discord.Role`.
This function takes into consideration the following cases:
@@ -453,15 +513,27 @@ class GuildChannel(Protocol):
- Channel overrides
- Member overrides
If a :class:`~discord.Role` is passed, then it checks the permissions
someone with that role would have, which is essentially:
- The default role permissions
- The default role permission overwrites
- The permission overwrites of the role used as a parameter
.. versionchanged:: 2.0
The object passed in can now be a role object.
Parameters
----------
member: :class:`~discord.Member`
The member to resolve permissions for.
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.
Returns
-------
:class:`~discord.Permissions`
The resolved permissions for the member.
The resolved permissions for the member or role.
"""
# The current cases can be explained as:
@@ -478,12 +550,35 @@ class GuildChannel(Protocol):
# The operation first takes into consideration the denied
# and then the allowed.
if self.guild.owner_id == member.id:
if self.guild.owner_id == obj.id:
return Permissions.all()
default = self.guild.default_role
base = Permissions(default.permissions.value)
roles = member._roles
# Handle the role case first
if isinstance(obj, Role):
if obj.is_default():
overwrite = utils.get(self._overwrites, type=_Overwrites.ROLE, id=obj.id)
if overwrite is not None:
base.handle_overwrite(overwrite.allow, overwrite.deny)
return base
denies = 0
allows = 0
guild_id = self.guild.id
for overwrite in self._overwrites:
if not overwrite.is_role():
continue
if overwrite.id in (obj.id, guild_id):
denies |= overwrite.deny
allows |= overwrite.allow
base.handle_overwrite(allows, denies)
return base
roles = obj._roles
get_role = self.guild.get_role
# Apply guild roles that the member has.
@@ -513,7 +608,7 @@ class GuildChannel(Protocol):
# Apply channel specific role permission overwrites
for overwrite in remaining_overwrites:
if overwrite.type == 'role' and roles.has(overwrite.id):
if overwrite.is_role() and roles.has(overwrite.id):
denies |= overwrite.deny
allows |= overwrite.allow
@@ -521,7 +616,7 @@ class GuildChannel(Protocol):
# Apply member specific permission overwrites
for overwrite in remaining_overwrites:
if overwrite.type == 'member' and overwrite.id == member.id:
if overwrite.is_member() and overwrite.id == obj.id:
base.handle_overwrite(allow=overwrite.allow, deny=overwrite.deny)
break
@@ -540,12 +635,12 @@ class GuildChannel(Protocol):
return base
async def delete(self, *, reason=None):
async def delete(self, *, reason: Optional[str] = None) -> None:
"""|coro|
Deletes the channel.
You must have :attr:`~Permissions.manage_channels` permission to use this.
You must have :attr:`~discord.Permissions.manage_channels` permission to use this.
Parameters
-----------
@@ -564,7 +659,34 @@ class GuildChannel(Protocol):
"""
await self._state.http.delete_channel(self.id, reason=reason)
async def set_permissions(self, target, *, overwrite=_undefined, reason=None, **permissions):
@overload
async def set_permissions(
self,
target: Union[Member, Role],
*,
overwrite: Optional[Union[PermissionOverwrite, _Undefined]] = ...,
reason: Optional[str] = ...,
) -> None:
...
@overload
async def set_permissions(
self,
target: Union[Member, Role],
*,
reason: Optional[str] = ...,
**permissions: bool,
) -> None:
...
async def set_permissions(
self,
target,
*,
overwrite=_undefined,
reason=None,
**permissions
):
r"""|coro|
Sets the channel specific permission overwrites for a target in the
@@ -582,7 +704,11 @@ class GuildChannel(Protocol):
If the ``overwrite`` parameter is ``None``, then the permission
overwrites are deleted.
You must have the :attr:`~Permissions.manage_roles` permission to use this.
You must have the :attr:`~discord.Permissions.manage_roles` permission to use this.
.. note::
This method *replaces* the old overwrites with the ones given.
Examples
----------
@@ -632,13 +758,13 @@ class GuildChannel(Protocol):
http = self._state.http
if isinstance(target, User):
perm_type = 'member'
perm_type = _Overwrites.MEMBER
elif isinstance(target, Role):
perm_type = 'role'
perm_type = _Overwrites.ROLE
else:
raise InvalidArgument('target parameter must be either Member or Role')
if isinstance(overwrite, _Undefined):
if overwrite is _undefined:
if len(permissions) == 0:
raise InvalidArgument('No overwrite provided.')
try:
@@ -659,10 +785,14 @@ class GuildChannel(Protocol):
else:
raise InvalidArgument('Invalid overwrite type provided.')
async def _clone_impl(self, base_attrs, *, name=None, reason=None):
base_attrs['permission_overwrites'] = [
x._asdict() for x in self._overwrites
]
async def _clone_impl(
self: GCH,
base_attrs: Dict[str, Any],
*,
name: Optional[str] = None,
reason: Optional[str] = None,
) -> GCH:
base_attrs['permission_overwrites'] = [x._asdict() for x in self._overwrites]
base_attrs['parent_id'] = self.category_id
base_attrs['name'] = name or self.name
guild_id = self.guild.id
@@ -674,7 +804,7 @@ class GuildChannel(Protocol):
self.guild._channels[obj.id] = obj
return obj
async def clone(self, *, name=None, reason=None):
async def clone(self: GCH, *, name: Optional[str] = None, reason: Optional[str] = None) -> GCH:
"""|coro|
Clones this channel. This creates a channel with the same properties
@@ -707,12 +837,60 @@ class GuildChannel(Protocol):
"""
raise NotImplementedError
async def move(self, **kwargs):
@overload
async def move(
self,
*,
beginning: bool,
offset: int = MISSING,
category: Optional[Snowflake] = MISSING,
sync_permissions: bool = MISSING,
reason: Optional[str] = MISSING,
) -> None:
...
@overload
async def move(
self,
*,
end: bool,
offset: int = MISSING,
category: Optional[Snowflake] = MISSING,
sync_permissions: bool = MISSING,
reason: str = MISSING,
) -> None:
...
@overload
async def move(
self,
*,
before: Snowflake,
offset: int = MISSING,
category: Optional[Snowflake] = MISSING,
sync_permissions: bool = MISSING,
reason: str = MISSING,
) -> None:
...
@overload
async def move(
self,
*,
after: Snowflake,
offset: int = MISSING,
category: Optional[Snowflake] = MISSING,
sync_permissions: bool = MISSING,
reason: str = MISSING,
) -> None:
...
async def move(self, **kwargs) -> None:
"""|coro|
A rich interface to help move a channel relative to other channels.
If exact position movement is required, :meth:`edit` should be used instead.
If exact position movement is required, ``edit`` should be used instead.
You must have the :attr:`~discord.Permissions.manage_channels` permission to
do this.
@@ -734,10 +912,10 @@ class GuildChannel(Protocol):
Whether to move the channel to the end of the
channel list (or category if given).
This is mutually exclusive with ``beginning``, ``before``, and ``after``.
before: :class:`abc.Snowflake`
before: :class:`~discord.abc.Snowflake`
The channel that should be before our current channel.
This is mutually exclusive with ``beginning``, ``end``, and ``after``.
after: :class:`abc.Snowflake`
after: :class:`~discord.abc.Snowflake`
The channel that should be after our current channel.
This is mutually exclusive with ``beginning``, ``end``, and ``before``.
offset: :class:`int`
@@ -747,7 +925,7 @@ class GuildChannel(Protocol):
while a negative number moves it above. Note that this
number is relative and computed after the ``beginning``,
``end``, ``before``, and ``after`` parameters.
category: Optional[:class:`abc.Snowflake`]
category: Optional[:class:`~discord.abc.Snowflake`]
The category to move this channel under.
If ``None`` is given then it moves it out of the category.
This parameter is ignored if moving a category channel.
@@ -776,8 +954,9 @@ class GuildChannel(Protocol):
raise InvalidArgument('Only one of [before, after, end, beginning] can be used.')
bucket = self._sorting_bucket
parent_id = kwargs.get('category', ...)
if parent_id not in (..., None):
parent_id = kwargs.get('category', MISSING)
# fmt: off
if parent_id not in (MISSING, None):
parent_id = parent_id.id
channels = [
ch
@@ -792,6 +971,7 @@ class GuildChannel(Protocol):
if ch._sorting_bucket == bucket
and ch.category_id == self.category_id
]
# fmt: on
channels.sort(key=lambda c: (c.position, c.id))
@@ -821,18 +1001,29 @@ class GuildChannel(Protocol):
reason = kwargs.get('reason')
for index, channel in enumerate(channels):
d = {'id': channel.id, 'position': index}
if parent_id is not ... and channel.id == self.id:
if parent_id is not MISSING and channel.id == self.id:
d.update(parent_id=parent_id, lock_permissions=lock_permissions)
payload.append(d)
await self._state.http.bulk_channel_update(self.guild.id, payload, reason=reason)
async def create_invite(self, *, reason=None, **fields):
async def create_invite(
self,
*,
reason: Optional[str] = None,
max_age: int = 0,
max_uses: int = 0,
temporary: bool = False,
unique: bool = True,
target_type: Optional[InviteTarget] = None,
target_user: Optional[User] = None,
target_application_id: Optional[int] = None
) -> Invite:
"""|coro|
Creates an instant invite from a text or voice channel.
You must have the :attr:`~Permissions.create_instant_invite` permission to
You must have the :attr:`~discord.Permissions.create_instant_invite` permission to
do this.
Parameters
@@ -852,6 +1043,20 @@ class GuildChannel(Protocol):
invite.
reason: Optional[:class:`str`]
The reason for creating this invite. Shows up on the audit log.
target_type: Optional[:class:`InviteTarget`]
The type of target for the voice channel invite, if any.
.. versionadded:: 2.0
target_user: Optional[:class:`User`]
The user whose stream to display for this invite, required if `target_type` is `TargetType.stream`. The user must be streaming in the channel.
.. versionadded:: 2.0
target_application_id:: Optional[:class:`int`]
The id of the embedded application for the invite, required if `target_type` is `TargetType.embedded_application`.
.. versionadded:: 2.0
Raises
-------
@@ -867,15 +1072,25 @@ class GuildChannel(Protocol):
The invite that was created.
"""
data = await self._state.http.create_invite(self.id, reason=reason, **fields)
data = await self._state.http.create_invite(
self.id,
reason=reason,
max_age=max_age,
max_uses=max_uses,
temporary=temporary,
unique=unique,
target_type=target_type.value if target_type else None,
target_user_id=target_user.id if target_user else None,
target_application_id=target_application_id
)
return Invite.from_incomplete(data=data, state=self._state)
async def invites(self):
async def invites(self) -> List[Invite]:
"""|coro|
Returns a list of all active instant invites from this channel.
You must have :attr:`~Permissions.manage_channels` to get this information.
You must have :attr:`~discord.Permissions.manage_channels` to get this information.
Raises
-------
@@ -892,14 +1107,8 @@ class GuildChannel(Protocol):
state = self._state
data = await state.http.invites_from_channel(self.id)
result = []
for invite in data:
invite['channel'] = self
invite['guild'] = self.guild
result.append(Invite(state=state, data=invite))
return result
guild = self.guild
return [Invite(state=state, data=invite, channel=self, guild=guild) for invite in data]
class Messageable(Protocol):
@@ -926,10 +1135,44 @@ class Messageable(Protocol):
async def _get_channel(self):
raise NotImplementedError
@overload
async def send(
self,
content: Optional[str] =...,
*,
tts: bool = ...,
embed: Embed = ...,
file: File = ...,
delete_after: int = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference] = ...,
mention_author: bool = ...,
view: View = ...,
) -> Message:
...
@overload
async def send(
self,
content: Optional[str] = ...,
*,
tts: bool = ...,
embed: Embed = ...,
files: List[File] = ...,
delete_after: int = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
reference: Union[Message, MessageReference] = ...,
mention_author: bool = ...,
view: View = ...,
) -> Message:
...
async def send(self, content=None, *, tts=False, embed=None, file=None,
files=None, delete_after=None, nonce=None,
allowed_mentions=None, reference=None,
mention_author=None):
mention_author=None, view=None):
"""|coro|
Sends a message to the destination with the content given.
@@ -987,6 +1230,10 @@ class Messageable(Protocol):
If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``.
.. versionadded:: 1.6
view: :class:`discord.ui.View`
A Discord UI View to add to the message.
.. versionadded:: 2.0
Raises
--------
@@ -1030,6 +1277,14 @@ class Messageable(Protocol):
except AttributeError:
raise InvalidArgument('reference parameter must be Message or MessageReference') from None
if view:
if not hasattr(view, '__discord_ui_view__'):
raise InvalidArgument(f'view parameter must be View not {view.__class__!r}')
components = view.to_components()
else:
components = None
if file is not None and files is not None:
raise InvalidArgument('cannot pass both file and files parameter to send()')
@@ -1040,7 +1295,7 @@ class Messageable(Protocol):
try:
data = await state.http.send_files(channel.id, files=[file], allowed_mentions=allowed_mentions,
content=content, tts=tts, embed=embed, nonce=nonce,
message_reference=reference)
message_reference=reference, components=components)
finally:
file.close()
@@ -1053,16 +1308,19 @@ class Messageable(Protocol):
try:
data = await state.http.send_files(channel.id, files=files, content=content, tts=tts,
embed=embed, nonce=nonce, allowed_mentions=allowed_mentions,
message_reference=reference)
message_reference=reference, components=components)
finally:
for f in files:
f.close()
else:
data = await state.http.send_message(channel.id, content, tts=tts, embed=embed,
nonce=nonce, allowed_mentions=allowed_mentions,
message_reference=reference)
message_reference=reference, components=components)
ret = state.create_message(channel=channel, data=data)
if view:
state.store_view(view, ret.id)
if delete_after is not None:
await ret.delete(delay=delete_after)
return ret
@@ -1089,10 +1347,11 @@ class Messageable(Protocol):
This means that both ``with`` and ``async with`` work with this.
Example Usage: ::
async with channel.typing():
# simulate something heavy
await asyncio.sleep(10)
async with channel.typing():
# do expensive stuff here
await channel.send('done!')
await channel.send('done!')
"""
return Typing(self)
@@ -1102,8 +1361,6 @@ class Messageable(Protocol):
Retrieves a single :class:`~discord.Message` from the destination.
This can only be used by bot accounts.
Parameters
------------
id: :class:`int`
@@ -1158,7 +1415,7 @@ class Messageable(Protocol):
def history(self, *, limit=100, before=None, after=None, around=None, oldest_first=None):
"""Returns an :class:`~discord.AsyncIterator` that enables receiving the destination's message history.
You must have :attr:`~Permissions.read_message_history` permissions to use this.
You must have :attr:`~discord.Permissions.read_message_history` permissions to use this.
Examples
---------
@@ -1223,12 +1480,14 @@ class Connectable(Protocol):
The following implement this ABC:
- :class:`~discord.VoiceChannel`
- :class:`~discord.StageChannel`
Note
----
This ABC is not decorated with :func:`typing.runtime_checkable`, so will fail :func:`isinstance`/:func:`issubclass`
checks.
"""
__slots__ = ()
def _get_voice_client_key(self):
@@ -1237,7 +1496,7 @@ class Connectable(Protocol):
def _get_voice_state_pair(self):
raise NotImplementedError
async def connect(self, *, timeout=60.0, reconnect=True, cls=VoiceClient):
async def connect(self, *, timeout: float = 60.0, reconnect: bool = True, cls: Type[T] = VoiceClient) -> T:
"""|coro|
Connects to voice and creates a :class:`VoiceClient` to establish

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 List, TYPE_CHECKING
from .asset import Asset
from .enums import ActivityType, try_enum
@@ -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,15 @@ t.ActivityFlags = {
}
"""
if TYPE_CHECKING:
from .types.activity import (
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,6 +117,7 @@ class BaseActivity:
.. versionadded:: 1.3
"""
__slots__ = ('_created_at',)
def __init__(self, **kwargs):
@@ -116,6 +132,7 @@ class BaseActivity:
if self._created_at is not None:
return datetime.datetime.utcfromtimestamp(self._created_at / 1000)
class Activity(BaseActivity):
"""Represents an activity in Discord.
@@ -164,27 +181,51 @@ 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.timestamps: ActivityTimestamps = kwargs.pop('timestamps', {})
self.assets: ActivityAssets = kwargs.pop('assets', {})
self.party: ActivityParty = 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.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)
@@ -269,6 +310,7 @@ class Activity(BaseActivity):
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."""
@@ -321,7 +363,7 @@ class Game(BaseActivity):
self.name = name
try:
timestamps = extra['timestamps']
timestamps: ActivityTimestamps = extra['timestamps']
except KeyError:
self._start = 0
self._end = 0
@@ -365,11 +407,13 @@ class Game(BaseActivity):
if 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):
return isinstance(other, Game) and other.name == self.name
@@ -380,6 +424,7 @@ class Game(BaseActivity):
def __hash__(self):
return hash(self.name)
class Streaming(BaseActivity):
"""A slimmed down version of :class:`Activity` that represents a Discord streaming status.
@@ -433,8 +478,8 @@ class Streaming(BaseActivity):
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.details = extra.pop('details', self.name) # compatibility
self.assets: ActivityAssets = extra.pop('assets', {})
@property
def type(self):
@@ -466,12 +511,14 @@ class Streaming(BaseActivity):
return name[7:] if name[:7] == 'twitch:' else None
def to_dict(self):
# fmt: off
ret = {
'type': ActivityType.streaming.value,
'name': str(self.name),
'url': str(self.url),
'assets': self.assets
}
# fmt: on
if self.details:
ret['details'] = self.details
return ret
@@ -485,6 +532,7 @@ class Streaming(BaseActivity):
def __hash__(self):
return hash(self.name)
class Spotify:
"""Represents a Spotify listening activity from Discord. This is a special case of
:class:`Activity` that makes it easier to work with the Spotify integration.
@@ -508,8 +556,7 @@ 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)
@@ -543,7 +590,7 @@ class Spotify:
""":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):
@@ -554,7 +601,7 @@ class Spotify:
def to_dict(self):
return {
'flags': 48, # SYNC | PLAY
'flags': 48, # SYNC | PLAY
'name': 'Spotify',
'assets': self._assets,
'party': self._party,
@@ -562,7 +609,7 @@ class Spotify:
'session_id': self._session_id,
'timestamps': self._timestamps,
'details': self._details,
'state': self._state
'state': self._state,
}
@property
@@ -571,8 +618,12 @@ class 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)
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):
return not self.__eq__(other)
@@ -584,7 +635,7 @@ class Spotify:
return 'Spotify'
def __repr__(self):
return '<Spotify title={0.title!r} artist={0.artist!r} track_id={0.track_id!r}>'.format(self)
return f'<Spotify title={self.title!r} artist={self.artist!r} track_id={self.track_id!r}>'
@property
def title(self):
@@ -644,6 +695,7 @@ class Spotify:
""":class:`str`: The party ID of the listening party."""
return self._party.get('id', '')
class CustomActivity(BaseActivity):
"""Represents a Custom activity from Discord.
@@ -721,7 +773,7 @@ class CustomActivity(BaseActivity):
return o
def __eq__(self, other):
return (isinstance(other, CustomActivity) and other.name == self.name and other.emoji == self.emoji)
return isinstance(other, CustomActivity) and other.name == self.name and other.emoji == self.emoji
def __ne__(self, other):
return not self.__eq__(other)
@@ -738,7 +790,7 @@ class CustomActivity(BaseActivity):
return str(self.name)
def __repr__(self):
return '<CustomActivity name={0.name!r} emoji={0.emoji!r}>'.format(self)
return f'<CustomActivity name={self.name!r} emoji={self.emoji!r}>'
def create_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',
'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,174 @@ 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.store_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:`~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,101 @@ 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
import yarl
__all__ = (
'Asset',
)
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"}
if TYPE_CHECKING:
ValidStaticFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png']
ValidAssetFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png', 'gif']
class Asset:
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {"gif"}
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 +129,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 +141,252 @@ class Asset:
Returns the hash of the asset.
"""
__slots__ = ('_state', '_url')
__slots__: Tuple[str, ...] = (
'_state',
'_url',
'_animated',
'_key',
)
BASE = 'https://cdn.discordapp.com'
def __init__(self, state, url=None):
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_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_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_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_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_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_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_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,
)
@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_sticker(cls, state, sticker_id: int, sticker_hash: str) -> Asset:
return cls(
state,
url=f'{cls.BASE}/stickers/{sticker_id}/{sticker_hash}.png?size=1024',
key=sticker_hash,
animated=False,
)
return cls(state, f'/emojis/{emoji.id}.{format}')
def __str__(self) -> str:
return self._url
def __str__(self):
return self.BASE + self._url if self._url is not None else ''
def __len__(self):
if self._url:
return len(self.BASE + self._url)
return 0
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}')
else:
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,12 +22,17 @@ 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',
@@ -35,40 +40,56 @@ __all__ = (
'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
from .types.audit_log import 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
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[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_owner_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_inviter_id(entry: AuditLogEntry, data: Optional[Snowflake]) -> Union[Member, User, None]:
if data is None:
return None
return entry._get_member(int(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'])
@@ -77,9 +98,10 @@ def _transform_overwrites(entry, data):
ow_type = elem['type']
ow_id = int(elem['id'])
if ow_type == 'role':
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,21 +111,66 @@ 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
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):
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]
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),
@@ -115,15 +182,25 @@ class AuditLogChanges:
'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),
'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)),
'type': (None, _enum_transformer(enums.ChannelType)),
}
# fmt: on
def __init__(self, entry, data):
def __init__(self, entry: AuditLogEntry, data: List[AuditLogChangePayload]):
self.before = AuditLogDiff()
self.after = AuditLogDiff()
@@ -132,18 +209,22 @@ class AuditLogChanges:
# special cases for role add/remove
if attr == '$add':
self._handle_role(self.before, self.after, entry, elem['new_value'])
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'])
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']
except KeyError:
@@ -168,16 +249,19 @@ class AuditLogChanges:
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):
def __repr__(self) -> str:
return f'<AuditLogChanges before={self.before!r} after={self.after!r}>'
def _handle_role(self, first, second, entry, elem):
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'])
@@ -185,12 +269,36 @@ class AuditLogChanges:
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)
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.
@@ -234,13 +342,13 @@ 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):
def _from_data(self, data: AuditLogEntryPayload) -> None:
self.action = enums.try_enum(enums.AuditLogAction, data['action_type'])
self.id = int(data['id'])
@@ -251,41 +359,58 @@ class AuditLogEntry(Hashable):
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'])
elems = {
'count': int(self.extra['count']),
'channel': self.guild.get_channel(channel_id) or Object(id=channel_id)
'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']),
}
self.extra = type('_AuditLogProxy', (), elems)()
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'])
elems = {
'channel': self.guild.get_channel(channel_id) or Object(id=channel_id),
'message_id': message_id
'message_id': int(self.extra['message_id']),
}
self.extra = type('_AuditLogProxy', (), elems)()
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':
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 }
@@ -294,22 +419,22 @@ class AuditLogEntry(Hashable):
# into meaningful data when requested
self._changes = data.get('changes', [])
self.user = self._get_member(utils._get_as_snowflake(data, 'user_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, Object, None]:
try:
converter = getattr(self, '_convert_target_' + self.action.target_type)
except AttributeError:
@@ -318,46 +443,40 @@ class AuditLogEntry(Hashable):
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
@@ -367,20 +486,18 @@ class AuditLogEntry(Hashable):
'max_uses': changeset.max_uses,
'code': changeset.code,
'temporary': changeset.temporary,
'channel': changeset.channel,
'uses': changeset.uses,
'guild': self.guild,
}
obj = Invite(state=self._state, data=fake_payload)
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)

View File

@@ -22,16 +22,23 @@ 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 asyncio
from typing import Callable, Dict, List, Optional, TYPE_CHECKING, Union, overload
import datetime
import discord.abc
from .permissions import Permissions
from .enums import ChannelType, try_enum, VoiceRegion
from .permissions import PermissionOverwrite, Permissions
from .enums import ChannelType, StagePrivacyLevel, try_enum, VoiceRegion, VideoQualityMode
from .mixins import Hashable
from . import utils
from .asset import Asset
from .errors import ClientException, NoMoreItems, InvalidArgument
from .stage_instance import StageInstance
from .threads import Thread
from .iterators import ArchivedThreadIterator
__all__ = (
'TextChannel',
@@ -41,9 +48,16 @@ __all__ = (
'CategoryChannel',
'StoreChannel',
'GroupChannel',
'_channel_factory',
)
if TYPE_CHECKING:
from .types.threads import ThreadArchiveDuration
from .role import Role
from .member import Member, VoiceState
from .abc import Snowflake, SnowflakeTime
from .message import Message
from .webhook import Webhook
async def _single_delete_strategy(messages):
for m in messages:
await m.delete()
@@ -92,6 +106,12 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
in this channel. A value of `0` denotes that it is disabled.
Bots and users with :attr:`~Permissions.manage_channels` or
:attr:`~Permissions.manage_messages` bypass slowmode.
nsfw: :class:`bool`
If the channel is marked as "not safe for work".
.. note::
To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead.
"""
__slots__ = ('name', 'id', 'guild', 'topic', '_state', 'nsfw',
@@ -155,6 +175,14 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
"""List[:class:`Member`]: Returns all members that can see this channel."""
return [m for m in self.guild.members if self.permissions_for(m).read_messages]
@property
def threads(self):
"""List[:class:`Thread`]: Returns all the threads that you can see.
.. versionadded:: 2.0
"""
return [thread for thread in self.guild.threads if thread.parent_id == self.id]
def is_nsfw(self):
""":class:`bool`: Checks if the channel is NSFW."""
return self.nsfw
@@ -184,6 +212,27 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
"""
return self._state._get_message(self.last_message_id) if self.last_message_id else None
@overload
async def edit(
self,
*,
reason: Optional[str] = ...,
name: str = ...,
topic: str = ...,
position: int = ...,
nsfw: bool = ...,
sync_permissions: bool = ...,
category: Optional[CategoryChannel] = ...,
slowmode_delay: int = ...,
type: ChannelType = ...,
overwrites: Dict[Union[Role, Member, Snowflake], PermissionOverwrite] = ...,
) -> None:
...
@overload
async def edit(self) -> None:
...
async def edit(self, *, reason=None, **options):
"""|coro|
@@ -240,7 +289,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
await self._edit(options, reason=reason)
@utils.copy_doc(discord.abc.GuildChannel.clone)
async def clone(self, *, name=None, reason=None):
async def clone(self, *, name: str = None, reason: str = None) -> TextChannel:
return await self._clone_impl({
'topic': self.topic,
'nsfw': self.nsfw,
@@ -263,8 +312,6 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
You must have the :attr:`~Permissions.manage_messages` permission to
use this.
Usable only by bot accounts.
Parameters
-----------
messages: Iterable[:class:`abc.Snowflake`]
@@ -275,8 +322,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
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.
You do not have proper permissions to delete the messages.
NotFound
If single delete, then the message was already deleted.
HTTPException
@@ -299,7 +345,17 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
message_ids = [m.id for m in messages]
await self._state.http.delete_messages(self.id, message_ids)
async def purge(self, *, limit=100, check=None, before=None, after=None, around=None, oldest_first=False, bulk=True):
async def purge(
self,
*,
limit: int = 100,
check: Callable[[Message], bool] = None,
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
@@ -307,8 +363,8 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
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
delete messages even if they are your own.
The :attr:`~Permissions.read_message_history` permission is
also needed to retrieve message history.
Examples
@@ -320,7 +376,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
return m.author == client.user
deleted = await channel.purge(limit=100, check=is_me)
await channel.send('Deleted {} message(s)'.format(len(deleted)))
await channel.send(f'Deleted {len(deleted)} message(s)')
Parameters
-----------
@@ -425,7 +481,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
data = await self._state.http.channel_webhooks(self.id)
return [Webhook.from_state(d, state=self._state) for d in data]
async def create_webhook(self, *, name, avatar=None, reason=None):
async def create_webhook(self, *, name: str, avatar: bytes = None, reason: str = None) -> Webhook:
"""|coro|
Creates a webhook for this channel.
@@ -465,7 +521,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
data = await self._state.http.create_webhook(self.id, name=str(name), avatar=avatar, reason=reason)
return Webhook.from_state(data, state=self._state)
async def follow(self, *, destination, reason=None):
async def follow(self, *, destination: TextChannel, reason: Optional[str] = None) -> Webhook:
"""
Follows a channel using a webhook.
@@ -504,7 +560,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
raise ClientException('The channel must be a news channel.')
if not isinstance(destination, TextChannel):
raise InvalidArgument('Expected TextChannel received {0.__name__}'.format(type(destination)))
raise InvalidArgument(f'Expected TextChannel received {destination.__class__.__name__}')
from .webhook import Webhook
data = await self._state.http.follow_webhook(self.id, webhook_channel_id=destination.id, reason=reason)
@@ -532,10 +588,146 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
from .message import PartialMessage
return PartialMessage(channel=self, id=message_id)
def get_thread(self, thread_id: int) -> Optional[Thread]:
"""Returns a thread with the given ID.
.. versionadded:: 2.0
Parameters
-----------
thread_id: :class:`int`
The ID to search for.
Returns
--------
Optional[:class:`Thread`]
The returned thread or ``None`` if not found.
"""
return self.guild.get_thread(thread_id)
async def start_thread(
self,
*,
name: str,
message: Optional[Snowflake] = None,
auto_archive_duration: ThreadArchiveDuration = 1440,
) -> Thread:
"""|coro|
Starts a thread in this text channel.
If no starter message is passed with the ``message`` parameter then
you must have :attr:`~discord.Permissions.send_messages` and
:attr:`~discord.Permissions.use_private_threads` in order to start the thread.
If a starter message is passed with the ``message`` parameter then
you must have :attr:`~discord.Permissions.send_messages` and
:attr:`~discord.Permissions.use_threads` in order to start the thread.
Parameters
-----------
name: :class:`str`
The name of the thread.
message: Optional[:class:`abc.Snowflake`]
A snowflake representing the message to start the thread with.
If ``None`` is passed then a private thread is started.
Defaults to ``None``.
auto_archive_duration: :class:`int`
The duration in minutes before a thread is automatically archived for inactivity.
Defaults to ``1440`` or 24 hours.
Raises
-------
Forbidden
You do not have permissions to start a thread.
HTTPException
Starting the thread failed.
"""
if message is None:
data = await self._state.http.start_private_thread(
self.id,
name=name,
auto_archive_duration=auto_archive_duration,
type=ChannelType.private_thread.value,
)
else:
data = await self._state.http.start_public_thread(
self.id,
message.id,
name=name,
auto_archive_duration=auto_archive_duration,
type=ChannelType.public_thread.value,
)
return Thread(guild=self.guild, data=data)
def archived_threads(
self,
*,
private: bool = False,
joined: bool = False,
limit: Optional[int] = 50,
before: Optional[Union[Snowflake, datetime.datetime]] = None,
) -> ArchivedThreadIterator:
"""Returns an :class:`~discord.AsyncIterator` that iterates over all archived threads in the guild.
You must have :attr:`~Permissions.read_message_history` to use this. If iterating over private threads
then :attr:`~Permissions.manage_threads` is also required.
Parameters
-----------
limit: Optional[:class:`bool`]
The number of threads to retrieve.
If ``None``, retrieves every archived thread in the channel. Note, however,
that this would make it a slow operation.
before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]]
Retrieve archived channels before the given date or ID.
private: :class:`bool`
Whether to retrieve private archived threads.
joined: :class:`bool`
Whether to retrieve private archived threads that you've joined.
You cannot set ``joined`` to ``True`` and ``private`` to ``False``.
Raises
------
Forbidden
You do not have permissions to get archived threads.
HTTPException
The request to get the archived threads failed.
Yields
-------
:class:`Thread`
The archived threads.
"""
return ArchivedThreadIterator(self.id, self.guild, limit=limit, joined=joined, private=private, before=before)
async def active_threads(self) -> List[Thread]:
"""|coro|
Returns a list of active :class:`Thread` that the client can access.
This includes both private and public threads.
Raises
------
HTTPException
The request to get the active threads failed.
Returns
--------
List[:class:`Thread`]
The archived threads
"""
data = await self._state.http.get_active_threads(self.id)
# TODO: thread members?
return [Thread(guild=self.guild, data=d) for d in data.get('threads', [])]
class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
__slots__ = ('name', 'id', 'guild', 'bitrate', 'user_limit',
'_state', 'position', '_overwrites', 'category_id',
'rtc_region')
'rtc_region', 'video_quality_mode')
def __init__(self, *, state, guild, data):
self._state = state
@@ -554,6 +746,7 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha
self.rtc_region = data.get('rtc_region')
if self.rtc_region:
self.rtc_region = try_enum(VoiceRegion, self.rtc_region)
self.video_quality_mode = try_enum(VideoQualityMode, data.get('video_quality_mode', 1))
self.category_id = utils._get_as_snowflake(data, 'parent_id')
self.position = data['position']
self.bitrate = data.get('bitrate')
@@ -565,7 +758,7 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha
return ChannelType.voice.value
@property
def members(self):
def members(self) -> List[Member]:
"""List[:class:`Member`]: Returns all members that are currently inside this voice channel."""
ret = []
for user_id, state in self.guild._voice_states.items():
@@ -576,7 +769,7 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha
return ret
@property
def voice_states(self):
def voice_states(self) -> Dict[int, VoiceState]:
"""Returns a mapping of member IDs who have voice states in this channel.
.. versionadded:: 1.3
@@ -594,7 +787,7 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha
return {key: value for key, value in self.guild._voice_states.items() if value.channel.id == self.id}
@utils.copy_doc(discord.abc.GuildChannel.permissions_for)
def permissions_for(self, member):
def permissions_for(self, member: Union[Role, Member], /) -> Permissions:
base = super().permissions_for(member)
# voice channels cannot be edited by people who can't connect to them
@@ -648,6 +841,10 @@ class VoiceChannel(VocalGuildChannel):
A value of ``None`` indicates automatic voice region detection.
.. versionadded:: 1.7
video_quality_mode: :class:`VideoQualityMode`
The camera video quality for the voice channel's participants.
.. versionadded:: 2.0
"""
__slots__ = ()
@@ -659,6 +856,7 @@ class VoiceChannel(VocalGuildChannel):
('rtc_region', self.rtc_region),
('position', self.position),
('bitrate', self.bitrate),
('video_quality_mode', self.video_quality_mode),
('user_limit', self.user_limit),
('category_id', self.category_id)
]
@@ -671,12 +869,33 @@ class VoiceChannel(VocalGuildChannel):
return ChannelType.voice
@utils.copy_doc(discord.abc.GuildChannel.clone)
async def clone(self, *, name=None, reason=None):
async def clone(self, *, name: str = None, reason: str = None) -> VoiceChannel:
return await self._clone_impl({
'bitrate': self.bitrate,
'user_limit': self.user_limit
}, name=name, reason=reason)
@overload
async def edit(
self,
*,
reason: Optional[str] = ...,
name: str = ...,
bitrate: int = ...,
user_limit: int = ...,
position: int = ...,
sync_permissions: int = ...,
category: Optional[CategoryChannel] = ...,
overwrites: Dict[Union[Role, Member], PermissionOverwrite] = ...,
rtc_region: Optional[VoiceRegion] = ...,
video_quality_mode: VideoQualityMode = ...,
) -> None:
...
@overload
async def edit(self) -> None:
...
async def edit(self, *, reason=None, **options):
"""|coro|
@@ -714,6 +933,10 @@ class VoiceChannel(VocalGuildChannel):
A value of ``None`` indicates automatic voice region detection.
.. versionadded:: 1.7
video_quality_mode: :class:`VideoQualityMode`
The camera video quality for the voice channel's participants.
.. versionadded:: 2.0
Raises
------
@@ -772,6 +995,10 @@ class StageChannel(VocalGuildChannel):
rtc_region: Optional[:class:`VoiceRegion`]
The region for the stage channel's voice communication.
A value of ``None`` indicates automatic voice region detection.
video_quality_mode: :class:`VideoQualityMode`
The camera video quality for the stage channel's participants.
.. versionadded:: 2.0
"""
__slots__ = ('topic',)
@@ -783,6 +1010,7 @@ class StageChannel(VocalGuildChannel):
('rtc_region', self.rtc_region),
('position', self.position),
('bitrate', self.bitrate),
('video_quality_mode', self.video_quality_mode),
('user_limit', self.user_limit),
('category_id', self.category_id)
]
@@ -794,20 +1022,139 @@ class StageChannel(VocalGuildChannel):
self.topic = data.get('topic')
@property
def requesting_to_speak(self):
def requesting_to_speak(self) -> List[Member]:
"""List[:class:`Member`]: A list of members who are requesting to speak in the stage channel."""
return [member for member in self.members if member.voice.requested_to_speak_at is not None]
@property
def speakers(self) -> List[Member]:
"""List[:class:`Member`]: A list of members who have been permitted to speak in the stage channel.
.. versionadded:: 2.0
"""
return [member for member in self.members if not member.voice.suppress and member.voice.requested_to_speak_at is None]
@property
def listeners(self) -> List[Member]:
"""List[:class:`Member`]: A list of members who are listening in the stage channel.
.. versionadded:: 2.0
"""
return [member for member in self.members if member.voice.suppress]
@property
def moderators(self) -> List[Member]:
"""List[:class:`Member`]: A list of members who are moderating the stage channel.
.. versionadded:: 2.0
"""
required_permissions = Permissions.stage_moderator()
return [member for member in self.members if self.permissions_for(member) >= required_permissions]
@property
def type(self):
""":class:`ChannelType`: The channel's Discord type."""
return ChannelType.stage_voice
@utils.copy_doc(discord.abc.GuildChannel.clone)
async def clone(self, *, name=None, reason=None):
return await self._clone_impl({
'topic': self.topic,
}, name=name, reason=reason)
async def clone(self, *, name: str = None, reason: Optional[str] = None) -> StageChannel:
return await self._clone_impl({}, name=name, reason=reason)
@property
def instance(self) -> Optional[StageInstance]:
"""Optional[:class:`StageInstance`]: The running stage instance of the stage channel.
.. versionadded:: 2.0
"""
return utils.get(self.guild.stage_instances, channel_id=self.id)
async def create_instance(self, *, topic: str, privacy_level: StagePrivacyLevel = utils.MISSING) -> StageInstance:
"""|coro|
Create a stage instance.
You must have the :attr:`~Permissions.manage_channels` permission to
use this.
.. versionadded:: 2.0
Parameters
-----------
topic: :class:`str`
The stage instance's topic.
privacy_level: :class:`StagePrivacyLevel`
The stage instance's privacy level. Defaults to :attr:`PrivacyLevel.guild_only`.
Raises
------
InvalidArgument
If the ``privacy_level`` parameter is not the proper type.
Forbidden
You do not have permissions to create a stage instance.
HTTPException
Creating a stage instance failed.
Returns
--------
:class:`StageInstance`
The newly created stage instance.
"""
payload = {
'channel_id': self.id,
'topic': topic
}
if privacy_level is not utils.MISSING:
if not isinstance(privacy_level, StagePrivacyLevel):
raise InvalidArgument('privacy_level field must be of type PrivacyLevel')
payload['privacy_level'] = privacy_level.value
data = await self._state.http.create_stage_instance(**payload)
return StageInstance(guild=self.guild, state=self._state, data=data)
async def fetch_instance(self) -> StageInstance:
"""|coro|
Gets the running :class:`StageInstance`.
.. versionadded:: 2.0
Raises
-------
:exc:`.NotFound`
The stage instance or channel could not be found.
:exc:`.HTTPException`
Getting the stage instance failed.
Returns
--------
:class:`StageInstance`
The stage instance.
"""
data = await self._state.http.get_stage_instance(self.id)
return StageInstance(guild=self.guild, state=self._state, data=data)
@overload
async def edit(
self,
*,
reason: Optional[str] = ...,
name: str = ...,
topic: Optional[str] = ...,
position: int = ...,
sync_permissions: int = ...,
category: Optional[CategoryChannel] = ...,
overwrites: Dict[Union[Role, Member], PermissionOverwrite] = ...,
rtc_region: Optional[VoiceRegion] = ...,
video_quality_mode: VideoQualityMode = ...,
) -> None:
...
@overload
async def edit(self) -> None:
...
async def edit(self, *, reason=None, **options):
"""|coro|
@@ -817,12 +1164,13 @@ class StageChannel(VocalGuildChannel):
You must have the :attr:`~Permissions.manage_channels` permission to
use this.
.. versionchanged:: 2.0
The ``topic`` parameter must now be set via :attr:`create_instance`.
Parameters
----------
name: :class:`str`
The new channel's name.
topic: :class:`str`
The new channel's topic.
position: :class:`int`
The new channel's position.
sync_permissions: :class:`bool`
@@ -839,6 +1187,10 @@ class StageChannel(VocalGuildChannel):
rtc_region: Optional[:class:`VoiceRegion`]
The new region for the stage channel's voice communication.
A value of ``None`` indicates automatic voice region detection.
video_quality_mode: :class:`VideoQualityMode`
The camera video quality for the stage channel's participants.
.. versionadded:: 2.0
Raises
------
@@ -851,7 +1203,6 @@ class StageChannel(VocalGuildChannel):
"""
await self._edit(options, reason=reason)
class CategoryChannel(discord.abc.GuildChannel, Hashable):
"""Represents a Discord channel category.
@@ -886,6 +1237,12 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
position: :class:`int`
The position in the category list. This is a number that starts at 0. e.g. the
top category is position 0.
nsfw: :class:`bool`
If the channel is marked as "not safe for work".
.. note::
To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead.
"""
__slots__ = ('name', 'id', 'guild', 'nsfw', '_state', 'position', '_overwrites', 'category_id')
@@ -896,7 +1253,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
self._update(guild, data)
def __repr__(self):
return '<CategoryChannel id={0.id} name={0.name!r} position={0.position} nsfw={0.nsfw}>'.format(self)
return f'<CategoryChannel id={self.id} name={self.name!r} position={self.position} nsfw={self.nsfw}>'
def _update(self, guild, data):
self.guild = guild
@@ -920,11 +1277,27 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
return self.nsfw
@utils.copy_doc(discord.abc.GuildChannel.clone)
async def clone(self, *, name=None, reason=None):
async def clone(self, *, name: str = None, reason: Optional[str] = None) -> CategoryChannel:
return await self._clone_impl({
'nsfw': self.nsfw
}, name=name, reason=reason)
@overload
async def edit(
self,
*,
reason: Optional[str] = ...,
name: str = ...,
position: int = ...,
nsfw: bool = ...,
overwrites: Dict[Union[Role, Member], PermissionOverwrite] = ...,
) -> None:
...
@overload
async def edit(self) -> None:
...
async def edit(self, *, reason=None, **options):
"""|coro|
@@ -1000,7 +1373,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
@property
def stage_channels(self):
"""List[:class:`StageChannel`]: Returns the voice channels that are under this category.
"""List[:class:`StageChannel`]: Returns the stage channels that are under this category.
.. versionadded:: 1.7
"""
@@ -1082,6 +1455,12 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
position: :class:`int`
The position in the channel list. This is a number that starts at 0. e.g. the
top channel is position 0.
nsfw: :class:`bool`
If the channel is marked as "not safe for work".
.. note::
To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead.
"""
__slots__ = ('name', 'id', 'guild', '_state', 'nsfw',
'category_id', 'position', '_overwrites',)
@@ -1092,7 +1471,7 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
self._update(guild, data)
def __repr__(self):
return '<StoreChannel id={0.id} name={0.name!r} position={0.position} nsfw={0.nsfw}>'.format(self)
return f'<StoreChannel id={self.id} name={self.name!r} position={self.position} nsfw={self.nsfw}>'
def _update(self, guild, data):
self.guild = guild
@@ -1125,11 +1504,29 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
return self.nsfw
@utils.copy_doc(discord.abc.GuildChannel.clone)
async def clone(self, *, name=None, reason=None):
async def clone(self, *, name: str = None, reason: Optional[str] = None) -> StoreChannel:
return await self._clone_impl({
'nsfw': self.nsfw
}, name=name, reason=reason)
@overload
async def edit(
self,
*,
name: str = ...,
position: int = ...,
nsfw: bool = ...,
sync_permissions: bool = ...,
category: Optional[CategoryChannel],
reason: Optional[str],
overwrites: Dict[Union[Role, Member], PermissionOverwrite]
) -> None:
...
@overload
async def edit(self) -> None:
...
async def edit(self, *, reason=None, **options):
"""|coro|
@@ -1195,8 +1592,10 @@ class DMChannel(discord.abc.Messageable, Hashable):
Attributes
----------
recipient: :class:`User`
recipient: Optional[:class:`User`]
The user you are participating with in the direct message channel.
If this channel is received through the gateway, the recipient information
may not be always available.
me: :class:`ClientUser`
The user presenting yourself.
id: :class:`int`
@@ -1215,10 +1614,21 @@ class DMChannel(discord.abc.Messageable, Hashable):
return self
def __str__(self):
return f'Direct Message with {self.recipient}'
if self.recipient:
return f'Direct Message with {self.recipient}'
return 'Direct Message with Unknown User'
def __repr__(self):
return '<DMChannel id={0.id} recipient={0.recipient!r}>'.format(self)
return f'<DMChannel id={self.id} recipient={self.recipient!r}>'
@classmethod
def _from_message(cls, state, channel_id):
self = cls.__new__(cls)
self._state = state
self.id = channel_id
self.recipient = None
self.me = state.user
return self
@property
def type(self):
@@ -1255,6 +1665,7 @@ class DMChannel(discord.abc.Messageable, Hashable):
"""
base = Permissions.text()
base.read_messages = True
base.send_tts_messages = False
base.manage_messages = False
return base
@@ -1312,13 +1723,11 @@ class GroupChannel(discord.abc.Messageable, Hashable):
The group channel ID.
owner: :class:`User`
The user that owns the group channel.
icon: Optional[:class:`str`]
The group channel's icon hash if provided.
name: Optional[:class:`str`]
The group channel's name if provided.
"""
__slots__ = ('id', 'recipients', 'owner', 'icon', 'name', 'me', '_state')
__slots__ = ('id', 'recipients', 'owner', '_icon', 'name', 'me', '_state')
def __init__(self, *, me, state, data):
self._state = state
@@ -1328,7 +1737,7 @@ class GroupChannel(discord.abc.Messageable, Hashable):
def _update_group(self, data):
owner_id = utils._get_as_snowflake(data, 'owner_id')
self.icon = data.get('icon')
self._icon = data.get('icon')
self.name = data.get('name')
try:
@@ -1354,7 +1763,7 @@ class GroupChannel(discord.abc.Messageable, Hashable):
return ', '.join(map(lambda x: x.name, self.recipients))
def __repr__(self):
return '<GroupChannel id={0.id} name={0.name!r}>'.format(self)
return f'<GroupChannel id={self.id} name={self.name!r}>'
@property
def type(self):
@@ -1362,40 +1771,11 @@ class GroupChannel(discord.abc.Messageable, Hashable):
return ChannelType.group
@property
def icon_url(self):
""":class:`Asset`: Returns the channel's icon asset if available.
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 channel 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, 'channel', format=format, size=size)
def icon(self):
"""Optional[:class:`Asset`]: Returns the channel's icon asset if available."""
if self._icon is None:
return None
return Asset._from_icon(self._state, self.id, self._icon, path='channel')
@property
def created_at(self):
@@ -1428,6 +1808,7 @@ class GroupChannel(discord.abc.Messageable, Hashable):
"""
base = Permissions.text()
base.read_messages = True
base.send_tts_messages = False
base.manage_messages = False
base.mention_everyone = True
@@ -1452,18 +1833,19 @@ class GroupChannel(discord.abc.Messageable, Hashable):
await self._state.http.leave_group(self.id)
def _channel_factory(channel_type):
value = try_enum(ChannelType, channel_type)
def _coerce_channel_type(value: Union[ChannelType, int]) -> ChannelType:
if isinstance(value, ChannelType):
return value
return try_enum(ChannelType, value)
def _guild_channel_factory(channel_type: Union[ChannelType, int]):
value = _coerce_channel_type(channel_type)
if value is ChannelType.text:
return TextChannel, value
elif value is ChannelType.voice:
return VoiceChannel, value
elif value is ChannelType.private:
return DMChannel, value
elif value is ChannelType.category:
return CategoryChannel, value
elif value is ChannelType.group:
return GroupChannel, value
elif value is ChannelType.news:
return TextChannel, value
elif value is ChannelType.store:
@@ -1472,3 +1854,12 @@ def _channel_factory(channel_type):
return StageChannel, value
else:
return None, value
def _channel_factory(channel_type: Union[ChannelType, int]):
cls, value = _guild_channel_factory(channel_type)
if value is ChannelType.private:
return DMChannel, value
elif value is ChannelType.group:
return GroupChannel, value
else:
return cls, value

View File

@@ -22,11 +22,14 @@ 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 logging
import signal
import sys
import traceback
from typing import Any, Generator, List, Optional, Sequence, TYPE_CHECKING, TypeVar, Union
import aiohttp
@@ -40,6 +43,7 @@ from .enums import ChannelType
from .mentions import AllowedMentions
from .errors import *
from .enums import Status, VoiceRegion
from .flags import ApplicationFlags
from .gateway import *
from .activity import BaseActivity, create_activity
from .voice_client import VoiceClient
@@ -51,11 +55,16 @@ from .backoff import ExponentialBackoff
from .webhook import Webhook
from .iterators import GuildIterator
from .appinfo import AppInfo
from .ui.view import View
from .stage_instance import StageInstance
__all__ = (
'Client',
)
if TYPE_CHECKING:
from .abc import SnowflakeTime
log = logging.getLogger(__name__)
def _cancel_tasks(loop):
@@ -131,8 +140,6 @@ class Client:
currently selected intents.
.. versionadded:: 1.5
fetch_offline_members: :class:`bool`
A deprecated alias of ``chunk_guilds_at_startup``.
chunk_guilds_at_startup: :class:`bool`
Indicates if :func:`.on_ready` should be delayed to chunk all guilds
at start-up if necessary. This operation is incredibly slow for large
@@ -158,37 +165,6 @@ class Client:
preparing the member cache and firing READY. The default timeout is 2 seconds.
.. versionadded:: 1.4
guild_subscriptions: :class:`bool`
Whether to dispatch presence or typing events. Defaults to ``True``.
.. versionadded:: 1.3
.. warning::
If this is set to ``False`` then the following features will be disabled:
- No user related updates (:func:`on_user_update` will not dispatch)
- All member related events will be disabled.
- :func:`on_member_update`
- :func:`on_member_join`
- :func:`on_member_remove`
- Typing events will be disabled (:func:`on_typing`).
- If ``fetch_offline_members`` is set to ``False`` then the user cache will not exist.
This makes it difficult or impossible to do many things, for example:
- Computing permissions
- Querying members in a voice channel via :attr:`VoiceChannel.members` will be empty.
- Most forms of receiving :class:`Member` will be
receiving :class:`User` instead, except for message events.
- :attr:`Guild.owner` will usually resolve to ``None``.
- :meth:`Guild.get_member` will usually be unavailable.
- Anything that involves using :class:`Member`.
- :attr:`users` will not be as populated.
- etc.
In short, this makes it so the only member you can reliably query is the
message author. Useful for bots that do not require any state.
assume_unsync_clock: :class:`bool`
Whether to assume the system clock is unsynced. This applies to the ratelimit handling
code. If this is set to ``True``, the default, then the library uses the time to reset
@@ -203,7 +179,7 @@ class Client:
ws
The websocket gateway the client is currently connected to. Could be ``None``.
loop: :class:`asyncio.AbstractEventLoop`
The event loop that the client uses for HTTP requests and websocket operations.
The event loop that the client uses for asynchronous operations.
"""
def __init__(self, *, loop=None, **options):
self.ws = None
@@ -318,10 +294,18 @@ class Client:
If this is not passed via ``__init__`` then this is retrieved
through the gateway when an event contains the data. Usually
after :func:`on_connect` is called.
after :func:`~discord.on_connect` is called.
"""
return self._connection.application_id
@property
def application_flags(self) -> ApplicationFlags:
""":class:`~discord.ApplicationFlags`: The client's application flags.
.. versionadded: 2.0
"""
return self._connection.application_flags # type: ignore
def is_ready(self):
""":class:`bool`: Specifies if the client's internal cache is ready for use."""
return self._ready.is_set()
@@ -431,14 +415,6 @@ class Client:
Logs in the client with the specified credentials.
This function can be used in two different ways.
.. warning::
Logging on with a user token is against the Discord
`Terms of Service <https://support.discord.com/hc/en-us/articles/115002192352>`_
and doing so might potentially get your account banned.
Use this at your own risk.
Parameters
-----------
@@ -551,7 +527,6 @@ class Client:
if self._closed:
return
await self.http.close()
self._closed = True
for voice in self.voice_clients:
@@ -564,6 +539,7 @@ class Client:
if self.ws is not None and self.ws.open:
await self.ws.close(code=1000)
await self.http.close()
self._ready.clear()
def clear(self):
@@ -690,7 +666,7 @@ class Client:
@property
def intents(self):
""":class:`Intents`: The intents configured for this connection.
""":class:`~discord.Intents`: The intents configured for this connection.
.. versionadded:: 1.5
"""
@@ -718,6 +694,28 @@ class Client:
"""
return self._connection.get_channel(id)
def get_stage_instance(self, id) -> Optional[StageInstance]:
"""Returns a stage instance with the given stage channel ID.
.. versionadded:: 2.0
Parameters
-----------
id: :class:`int`
The ID to search for.
Returns
--------
Optional[:class:`StageInstance`]
The returns stage instance of ``None`` if not found.
"""
from .channel import StageChannel
channel = self._connection.get_channel(id)
if isinstance(channel, StageChannel):
return channel.instance
def get_guild(self, id):
"""Returns a guild with the given ID.
@@ -849,7 +847,7 @@ class Client:
return m.content == 'hello' and m.channel == channel
msg = await client.wait_for('message', check=check)
await channel.send('Hello {.author}!'.format(msg))
await channel.send(f'Hello {msg.author}!')
Waiting for a thumbs up reaction from the message author: ::
@@ -999,7 +997,7 @@ class Client:
# Guild stuff
def fetch_guilds(self, *, limit=100, before=None, after=None):
def fetch_guilds(self, *, limit: int = 100, before: SnowflakeTime = None, after: SnowflakeTime = None) -> List[Guild]:
"""Retrieves an :class:`.AsyncIterator` that enables receiving your guilds.
.. note::
@@ -1078,7 +1076,7 @@ class Client:
"""
code = utils.resolve_template(code)
data = await self.http.get_template(code)
return Template(data=data, state=self._connection)
return Template(data=data, state=self._connection) # type: ignore
async def fetch_guild(self, guild_id):
"""|coro|
@@ -1114,7 +1112,7 @@ class Client:
data = await self.http.get_guild(guild_id)
return Guild(data=data, state=self._connection)
async def create_guild(self, name, region=None, icon=None, *, code=None):
async def create_guild(self, name: str, region: Optional[VoiceRegion] = None, icon: Any = None, *, code: str = None):
"""|coro|
Creates a :class:`.Guild`.
@@ -1161,9 +1159,37 @@ class Client:
data = await self.http.create_guild(name, region_value, icon)
return Guild(data=data, state=self._connection)
async def fetch_stage_instance(self, channel_id: int) -> StageInstance:
"""|coro|
Gets a :class:`StageInstance` for a stage channel id.
.. versionadded:: 2.0
Parameters
-----------
channel_id: :class:`int`
The stage channel ID.
Raises
-------
:exc:`.NotFound`
The stage instance or channel could not be found.
:exc:`.HTTPException`
Getting the stage instance failed.
Returns
--------
:class:`StageInstance`
The stage instance from the stage channel ID.
"""
data = await self.http.get_stage_instance(channel_id)
guild = self.get_guild(int(data['guild_id']))
return StageInstance(guild=guild, state=self._connection, data=data) # type: ignore
# Invite management
async def fetch_invite(self, url, *, with_counts=True):
async def fetch_invite(self, url: Union[Invite, str], *, with_counts: bool = True, with_expiration: bool = True) -> Invite:
"""|coro|
Gets an :class:`.Invite` from a discord.gg URL or ID.
@@ -1182,6 +1208,11 @@ class Client:
Whether to include count information in the invite. This fills the
:attr:`.Invite.approximate_member_count` and :attr:`.Invite.approximate_presence_count`
fields.
with_expiration: :class:`bool`
Whether to include the expiration date of the invite. This fills the
:attr:`.Invite.expires_at` field.
.. versionadded:: 2.0
Raises
-------
@@ -1197,10 +1228,10 @@ class Client:
"""
invite_id = utils.resolve_invite(url)
data = await self.http.get_invite(invite_id, with_counts=with_counts)
data = await self.http.get_invite(invite_id, with_counts=with_counts, with_expiration=with_expiration)
return Invite.from_incomplete(state=self._connection, data=data)
async def delete_invite(self, invite):
async def delete_invite(self, invite: Union[Invite, str]) -> None:
"""|coro|
Revokes an :class:`.Invite`, URL, or ID to an invite.
@@ -1281,14 +1312,13 @@ class Client:
async def fetch_user(self, user_id):
"""|coro|
Retrieves a :class:`~discord.User` based on their ID. This can only
be used by bot accounts. You do not have to share any guilds
with the user to get this information, however many operations
do require that you do.
Retrieves a :class:`~discord.User` based on their ID.
You do not have to share any guilds with the user to get this information,
however many operations do require that you do.
.. note::
This method is an API call. For general usage, consider :meth:`get_user` instead.
This method is an API call. If you have :attr:`discord.Intents.members` and member cache enabled, consider :meth:`get_user` instead.
Parameters
-----------
@@ -1373,3 +1403,68 @@ class Client:
"""
data = await self.http.get_webhook(webhook_id)
return Webhook.from_state(data, state=self._connection)
async def create_dm(self, user):
"""|coro|
Creates a :class:`.DMChannel` with this user.
This should be rarely called, as this is done transparently for most
people.
.. versionadded:: 2.0
Parameters
-----------
user: :class:`~discord.abc.Snowflake`
The user to create a DM with.
Returns
-------
:class:`.DMChannel`
The channel that was created.
"""
state = self._connection
found = state._get_private_channel_by_user(user.id)
if found:
return found
data = await state.http.start_private_message(user.id)
return state.add_dm_channel(data)
def add_view(self, view: View, *, message_id: Optional[int] = None) -> None:
"""Registers a :class:`~discord.ui.View` for persistent listening.
This method should be used for when a view is comprised of components
that last longer than the lifecycle of the program.
Parameters
------------
view: :class:`discord.ui.View`
The view to register for dispatching.
message_id: Optional[:class:`int`]
The message ID that the view is attached to. This is currently used to
refresh the view's state during message update events. If not given
then message update events are not propagated for the view.
Raises
-------
TypeError
A view was not passed.
ValueError
The view is not persistent. A persistent view has no timeout
and all their components have an explicitly provided custom_id.
"""
if not isinstance(view, View):
raise TypeError(f'expected an instance of View not {view.__class__!r}')
if not view.is_persistent():
raise ValueError('View is not persistent. Items need to have a custom_id set and View must have no timeout')
self._connection.store_view(view, message_id)
@property
def persistent_views(self) -> Sequence[View]:
"""Sequence[:class:`View`]: A sequence of persistent views added to the client."""
return self._connection.persistent_views

View File

@@ -25,11 +25,23 @@ DEALINGS IN THE SOFTWARE.
import colorsys
import random
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,6 +66,10 @@ class Colour:
Returns the hex format for the colour.
.. describe:: int(x)
Returns the raw colour value.
Attributes
------------
value: :class:`int`
@@ -66,63 +82,66 @@ class Colour:
if not isinstance(value, int):
raise TypeError(f'Expected int parameter, received {value.__class__.__name__} instead.')
self.value = value
self.value: int = value
def _get_byte(self, byte):
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):
def __str__(self) -> str:
return f'#{self.value:0>6x}'
def __repr__(self):
def __int__(self) -> int:
return self.value
def __repr__(self) -> str:
return f'<Colour value={self.value}>'
def __hash__(self):
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,130 @@ 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)
@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)
@classmethod
def green(cls):
def green(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``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)
@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)
@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)
@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)
@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)
@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)
@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)
@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)
@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)
@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)
@classmethod
def red(cls):
def red(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``."""
return cls(0xe74c3c)
@classmethod
def dark_red(cls):
def dark_red(cls: Type[CT]) -> CT:
"""A factory method that returns a :class:`Colour` with a value of ``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)
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)
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)
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)
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)
@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)
@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 +293,21 @@ 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)
Color = Colour

358
discord/components.py Normal file
View File

@@ -0,0 +1,358 @@
"""
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`.
.. versionadded:: 2.0
Attributes
-----------
style: :class:`ComponentButtonStyle`
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.
.. 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.
"""
__slots__: Tuple[str, ...] = (
'custom_id',
'placeholder',
'min_values',
'max_values',
'options',
)
__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', [])]
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],
}
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 25 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 50 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}>'
)
@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

@@ -179,10 +179,10 @@ class Embed:
*,
colour: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
color: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
title: MaybeEmpty[str] = EmptyEmbed,
title: MaybeEmpty[Any] = EmptyEmbed,
type: EmbedType = 'rich',
url: MaybeEmpty[str] = EmptyEmbed,
description: MaybeEmpty[str] = EmptyEmbed,
url: MaybeEmpty[Any] = EmptyEmbed,
description: MaybeEmpty[Any] = EmptyEmbed,
timestamp: datetime.datetime = None,
):
@@ -273,11 +273,11 @@ class Embed:
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
@@ -342,7 +342,7 @@ class Embed:
"""
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
@@ -364,7 +364,22 @@ class Embed:
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
@property
def image(self) -> _EmbedMediaProxy:
"""Returns an ``EmbedProxy`` denoting the image contents.
@@ -380,7 +395,7 @@ class Embed:
"""
return EmbedProxy(getattr(self, '_image', {})) # type: ignore
def set_image(self: E, *, url: MaybeEmpty[str]) -> E:
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
@@ -422,7 +437,7 @@ class Embed:
"""
return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore
def set_thumbnail(self: E, *, url: MaybeEmpty[str]) -> E:
def set_thumbnail(self: E, *, url: MaybeEmpty[Any]) -> E:
"""Sets the thumbnail for the embed content.
This function returns the class instance to allow for fluent-style
@@ -483,7 +498,7 @@ class Embed:
"""
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
@@ -528,7 +543,7 @@ 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.
@@ -536,7 +551,7 @@ class Embed:
"""
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
@@ -565,7 +580,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
@@ -626,7 +641,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.

View File

@@ -22,16 +22,28 @@ 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',
)
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
@@ -80,68 +92,76 @@ 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', [])))
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 = User(state=self._state, data=user) if user else None
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] != '_':
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 __repr__(self) -> str:
return f'<Emoji id={self.id} name={self.name!r} animated={self.animated} managed={self.managed}>'
def __eq__(self, other):
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 +173,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 +189,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 +212,7 @@ 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) -> None:
r"""|coro|
Edits the custom emoji.
@@ -237,8 +224,8 @@ class Emoji(_EmojiTag):
-----------
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.
@@ -250,7 +237,10 @@ class Emoji(_EmojiTag):
An error occurred editing the 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]
await self._state.http.edit_custom_emoji(self.guild.id, self.id, payload=payload, reason=reason)

View File

@@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
import types
from collections import namedtuple
from typing import Any, TYPE_CHECKING, Type, TypeVar
from typing import Any, Dict, Optional, TYPE_CHECKING, Type, TypeVar
__all__ = (
'Enum',
@@ -46,6 +46,14 @@ __all__ = (
'ExpireBehaviour',
'ExpireBehavior',
'StickerType',
'InviteTarget',
'VideoQualityMode',
'ComponentType',
'ButtonStyle',
'StagePrivacyLevel',
'InteractionType',
'InteractionResponseType',
'NSFWLevel',
)
def _create_value_cls(name):
@@ -147,14 +155,17 @@ else:
return value
class ChannelType(Enum):
text = 0
private = 1
voice = 2
group = 3
category = 4
news = 5
store = 6
stage_voice = 13
text = 0
private = 1
voice = 2
group = 3
category = 4
news = 5
store = 6
news_thread = 10
public_thread = 11
private_thread = 12
stage_voice = 13
def __str__(self):
return self.name
@@ -178,6 +189,11 @@ class MessageType(Enum):
guild_discovery_requalified = 15
guild_discovery_grace_period_initial_warning = 16
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'
@@ -220,14 +236,11 @@ class SpeakingState(Enum):
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
none = 0
low = 1
medium = 2
high = 3
highest = 4
def __str__(self):
return self.name
@@ -307,50 +320,56 @@ class AuditLogAction(Enum):
integration_create = 80
integration_update = 81
integration_delete = 82
stage_instance_create = 83
stage_instance_update = 84
stage_instance_delete = 85
@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]:
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,
}
return lookup[self]
@property
def target_type(self):
def target_type(self) -> Optional[str]:
v = self.value
if v == -1:
return 'all'
@@ -368,10 +387,14 @@ class AuditLogAction(Enum):
return 'webhook'
elif v < 70:
return 'emoji'
elif v == 73:
return 'channel'
elif v < 80:
return 'message'
elif v < 90:
elif v < 83:
return 'integration'
elif v < 90:
return 'stage_instance'
class UserFlags(Enum):
staff = 1
@@ -390,6 +413,7 @@ class UserFlags(Enum):
bug_hunter_level_2 = 16384
verified_bot = 65536
verified_bot_developer = 131072
discord_certified_moderator = 262144
class ActivityType(Enum):
unknown = -1
@@ -410,6 +434,7 @@ class TeamMembershipState(Enum):
class WebhookType(Enum):
incoming = 1
channel_follower = 2
application = 3
class ExpireBehaviour(Enum):
remove_role = 0
@@ -422,9 +447,66 @@ class StickerType(Enum):
apng = 2
lottie = 3
class InviteTarget(Enum):
unknown = 0
stream = 1
embedded_application = 2
class InteractionType(Enum):
ping = 1
application_command = 2
component = 3
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
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
green = 3
red = 4
def __int__(self):
return self.value
class StagePrivacyLevel(Enum):
public = 1
closed = 2
guild_only = 2
class NSFWLevel(Enum):
default = 0
explicit = 1
safe = 2
age_restricted = 3
T = TypeVar('T')

View File

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

View File

@@ -121,11 +121,6 @@ class BotBase(GroupMixin):
if self.owner_ids and not isinstance(self.owner_ids, collections.abc.Collection):
raise TypeError(f'owner_ids must be a collection not {self.owner_ids.__class__!r}')
if options.pop('self_bot', False):
self._skip_check = lambda x, y: x != y
else:
self._skip_check = lambda x, y: x == y
if help_command is _default:
self.help_command = DefaultHelpCommand()
else:
@@ -167,11 +162,12 @@ class BotBase(GroupMixin):
if self.extra_events.get('on_command_error', None):
return
if hasattr(context.command, 'on_error'):
command = context.command
if command and command.has_error_handler():
return
cog = context.cog
if cog and Cog._get_overridden_method(cog.cog_command_error) is not None:
if cog and cog.has_error_handler():
return
print(f'Ignoring exception in command {context.command}:', file=sys.stderr)
@@ -219,7 +215,7 @@ class BotBase(GroupMixin):
The function that was used as a global check.
call_once: :class:`bool`
If the function should only be called once per
:meth:`.Command.invoke` call.
:meth:`.invoke` call.
"""
if call_once:
@@ -252,7 +248,7 @@ class BotBase(GroupMixin):
r"""A decorator that adds a "call once" global check to the bot.
Unlike regular global checks, this one is called only once
per :meth:`.Command.invoke` call.
per :meth:`.invoke` call.
Regular global checks are called whenever a command is called
or :meth:`.Command.can_run` is called. This type of check
@@ -488,15 +484,25 @@ class BotBase(GroupMixin):
# cogs
def add_cog(self, cog):
def add_cog(self, cog: Cog, *, override: bool = False) -> None:
"""Adds a "cog" to the bot.
A cog is a class that has its own event listeners and commands.
.. versionchanged:: 2.0
:exc:`.ClientException` is raised when a cog with the same name
is already loaded.
Parameters
-----------
cog: :class:`.Cog`
The cog to register to the bot.
override: :class:`bool`
If a previously loaded cog with the same name should be ejected
instead of raising an error.
.. versionadded:: 2.0
Raises
-------
@@ -504,13 +510,23 @@ class BotBase(GroupMixin):
The cog does not inherit from :class:`.Cog`.
CommandError
An error happened during loading.
.ClientException
A cog with the same name is already loaded.
"""
if not isinstance(cog, Cog):
raise TypeError('cogs must derive from Cog')
cog_name = cog.__cog_name__
existing = self.__cogs.get(cog_name)
if existing is not None:
if not override:
raise discord.ClientException(f'Cog named {cog_name!r} already loaded')
self.remove_cog(cog_name)
cog = cog._inject(self)
self.__cogs[cog.__cog_name__] = cog
self.__cogs[cog_name] = cog
def get_cog(self, name):
"""Gets the cog instance requested.
@@ -845,7 +861,7 @@ class BotBase(GroupMixin):
raise
raise TypeError("command_prefix must be plain string, iterable of strings, or callable "
"returning either of these, not {}".format(ret.__class__.__name__))
f"returning either of these, not {ret.__class__.__name__}")
if not ret:
raise ValueError("Iterable command_prefix must contain at least one prefix")
@@ -885,7 +901,7 @@ class BotBase(GroupMixin):
view = StringView(message.content)
ctx = cls(prefix=None, view=view, bot=self, message=message)
if self._skip_check(message.author.id, self.user.id):
if message.author.id == self.user.id:
return ctx
prefix = await self.get_prefix(message)
@@ -906,13 +922,13 @@ class BotBase(GroupMixin):
except TypeError:
if not isinstance(prefix, list):
raise TypeError("get_prefix must return either a string or a list of string, "
"not {}".format(prefix.__class__.__name__))
f"not {prefix.__class__.__name__}")
# It's possible a bad command_prefix got us here.
for value in prefix:
if not isinstance(value, str):
raise TypeError("Iterable command_prefix or list returned from get_prefix must "
"contain only strings, not {}".format(value.__class__.__name__))
f"contain only strings, not {value.__class__.__name__}")
# Getting here shouldn't happen
raise
@@ -1030,10 +1046,6 @@ class Bot(BotBase, discord.Client):
you require group commands to be case insensitive as well.
description: :class:`str`
The content prefixed into the default help message.
self_bot: :class:`bool`
If ``True``, the bot will only listen to commands invoked by itself rather
than ignoring itself. If ``False`` (the default) then the bot will ignore
itself. This cannot be changed once initialised.
help_command: Optional[:class:`.HelpCommand`]
The help command implementation to use. This can be dynamically
set at runtime. To remove the help command pass ``None``. For more

View File

@@ -334,7 +334,7 @@ class Cog(metaclass=CogMeta):
@_cog_special_method
def cog_check(self, ctx):
"""A special method that registers as a :func:`commands.check`
"""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,

View File

@@ -24,6 +24,7 @@ DEALINGS IN THE SOFTWARE.
import discord.abc
import discord.utils
import re
__all__ = (
'Context',
@@ -46,12 +47,17 @@ 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.
: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: :class:`str`
The prefix that was used to invoke the command.
command: :class:`Command`
@@ -93,6 +99,7 @@ class Context(discord.abc.Messageable):
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.current_parameter = attrs.pop('current_parameter', None)
self._state = self.message._state
async def invoke(self, command, /, *args, **kwargs):
@@ -136,7 +143,7 @@ class Context(discord.abc.Messageable):
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):
"""|coro|
Calls the command again.
@@ -206,6 +213,20 @@ class Context(discord.abc.Messageable):
async def _get_channel(self):
return self.channel
@property
def clean_prefix(self):
""":class:`str`: The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``.
.. versionadded:: 2.0
"""
user = self.guild.me if self.guild else self.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(r"<@!?%s>" % user.id)
return pattern.sub("@%s" % user.display_name.replace('\\', r'\\'), self.prefix)
@property
def cog(self):
"""Optional[:class:`.Cog`]: Returns the cog associated with this context's command. None if it does not exist."""

View File

@@ -26,7 +26,22 @@ from __future__ import annotations
import re
import inspect
from typing import TYPE_CHECKING, Generic, Protocol, TypeVar, Union, runtime_checkable
from typing import (
Any,
Dict,
Generic,
Iterable,
Literal,
Optional,
TYPE_CHECKING,
List,
Protocol,
Type,
TypeVar,
Tuple,
Union,
runtime_checkable,
)
import discord
from .errors import *
@@ -37,6 +52,7 @@ if TYPE_CHECKING:
__all__ = (
'Converter',
'ObjectConverter',
'MemberConverter',
'UserConverter',
'MessageConverter',
@@ -55,8 +71,10 @@ __all__ = (
'CategoryChannelConverter',
'IDConverter',
'StoreChannelConverter',
'GuildChannelConverter',
'clean_content',
'Greedy',
'run_converters',
)
@@ -70,11 +88,13 @@ def _get_from_guilds(bot, getter, argument):
_utils_get = discord.utils.get
T = TypeVar('T', covariant=True)
T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
CT = TypeVar('CT', bound=discord.abc.GuildChannel)
@runtime_checkable
class Converter(Protocol[T]):
class Converter(Protocol[T_co]):
"""The base class of custom converters that require the :class:`.Context`
to be passed to be useful.
@@ -85,7 +105,7 @@ class Converter(Protocol[T]):
method to do its conversion logic. This method must be a :ref:`coroutine <coroutine>`.
"""
async def convert(self, ctx: Context, argument: str) -> T:
async def convert(self, ctx: Context, argument: str) -> T_co:
"""|coro|
The method to override to do conversion logic.
@@ -110,13 +130,39 @@ class Converter(Protocol[T]):
"""
raise NotImplementedError('Derived classes need to implement this.')
class IDConverter(Converter[T]):
def __init__(self):
self._id_regex = re.compile(r'([0-9]{15,20})$')
super().__init__()
def _get_id_match(self, argument):
return self._id_regex.match(argument)
_ID_REGEX = re.compile(r'([0-9]{15,20})$')
class IDConverter(Converter[T_co]):
@staticmethod
def _get_id_match(argument):
return _ID_REGEX.match(argument)
class ObjectConverter(IDConverter[discord.Object]):
"""Converts to a :class:`~discord.Object`.
The argument must follow the valid ID or mention formats (e.g. `<@80088516616269824>`).
.. versionadded:: 2.0
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by member, role, or channel mention.
"""
async def convert(self, ctx: Context, argument: str) -> discord.Object:
match = self._get_id_match(argument) or re.match(r'<(?:@(?:!|&)?|#)([0-9]{15,20})>$', argument)
if match is None:
raise ObjectNotFound(argument)
result = int(match.group(1))
return discord.Object(id=result)
class MemberConverter(IDConverter[discord.Member]):
"""Converts to a :class:`~discord.Member`.
@@ -173,7 +219,7 @@ class MemberConverter(IDConverter[discord.Member]):
async def convert(self, ctx: Context, argument: str) -> discord.Member:
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]{15,20})>$', argument)
guild = ctx.guild
result = None
user_id = None
@@ -204,6 +250,7 @@ class MemberConverter(IDConverter[discord.Member]):
return result
class UserConverter(IDConverter[discord.User]):
"""Converts to a :class:`~discord.User`.
@@ -223,8 +270,9 @@ class UserConverter(IDConverter[discord.User]):
This converter now lazily fetches users from the HTTP APIs if an ID is passed
and it's not available in cache.
"""
async def convert(self, ctx: Context, argument: str) -> discord.User:
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]{15,20})>$', argument)
result = None
state = ctx._state
@@ -263,6 +311,7 @@ class UserConverter(IDConverter[discord.User]):
return result
class PartialMessageConverter(Converter[discord.PartialMessage]):
"""Converts to a :class:`discord.PartialMessage`.
@@ -274,6 +323,7 @@ class PartialMessageConverter(Converter[discord.PartialMessage]):
2. By message ID (The message is assumed to be in the context channel.)
3. By message URL
"""
@staticmethod
def _get_id_matches(argument):
id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,20})-)?(?P<message_id>[0-9]{15,20})$')
@@ -285,8 +335,8 @@ class PartialMessageConverter(Converter[discord.PartialMessage]):
match = id_regex.match(argument) or link_regex.match(argument)
if not match:
raise MessageNotFound(argument)
channel_id = match.group("channel_id")
return int(match.group("message_id")), int(channel_id) if channel_id else None
channel_id = match.group('channel_id')
return int(match.group('message_id')), int(channel_id) if channel_id else None
async def convert(self, ctx: Context, argument: str) -> discord.PartialMessage:
message_id, channel_id = self._get_id_matches(argument)
@@ -295,6 +345,7 @@ class PartialMessageConverter(Converter[discord.PartialMessage]):
raise ChannelNotFound(channel_id)
return discord.PartialMessage(channel=channel, id=message_id)
class MessageConverter(IDConverter[discord.Message]):
"""Converts to a :class:`discord.Message`.
@@ -309,6 +360,7 @@ class MessageConverter(IDConverter[discord.Message]):
.. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: Context, argument: str) -> discord.Message:
message_id, channel_id = PartialMessageConverter._get_id_matches(argument)
message = ctx.bot._connection._get_message(message_id)
@@ -324,6 +376,56 @@ class MessageConverter(IDConverter[discord.Message]):
except discord.Forbidden:
raise ChannelNotReadable(channel)
class GuildChannelConverter(IDConverter[discord.abc.GuildChannel]):
"""Converts to a :class:`~discord.abc.GuildChannel`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name.
.. versionadded:: 2.0
"""
async def convert(self, ctx: Context, argument: str) -> discord.abc.GuildChannel:
return self._resolve_channel(ctx, argument, ctx.guild.channels, discord.abc.GuildChannel)
@staticmethod
def _resolve_channel(ctx: Context, argument: str, iterable: Iterable[CT], type: Type[CT]) -> CT:
bot = ctx.bot
match = IDConverter._get_id_match(argument) or re.match(r'<#([0-9]{15,20})>$', argument)
result = None
guild = ctx.guild
if match is None:
# not a mention
if guild:
result: Optional[CT] = discord.utils.get(iterable, name=argument)
else:
def check(c):
return isinstance(c, type) and c.name == argument
result = discord.utils.find(check, bot.get_all_channels())
else:
channel_id = int(match.group(1))
if guild:
result = guild.get_channel(channel_id)
else:
result = _get_from_guilds(bot, 'get_channel', channel_id)
if not isinstance(result, type):
raise ChannelNotFound(argument)
return result
class TextChannelConverter(IDConverter[discord.TextChannel]):
"""Converts to a :class:`~discord.TextChannel`.
@@ -339,32 +441,10 @@ class TextChannelConverter(IDConverter[discord.TextChannel]):
.. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: Context, argument: str) -> discord.TextChannel:
bot = ctx.bot
return GuildChannelConverter._resolve_channel(ctx, argument, ctx.guild.text_channels, discord.TextChannel)
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
result = None
guild = ctx.guild
if match is None:
# not a mention
if guild:
result = discord.utils.get(guild.text_channels, name=argument)
else:
def check(c):
return isinstance(c, discord.TextChannel) and c.name == argument
result = discord.utils.find(check, bot.get_all_channels())
else:
channel_id = int(match.group(1))
if guild:
result = guild.get_channel(channel_id)
else:
result = _get_from_guilds(bot, 'get_channel', channel_id)
if not isinstance(result, discord.TextChannel):
raise ChannelNotFound(argument)
return result
class VoiceChannelConverter(IDConverter[discord.VoiceChannel]):
"""Converts to a :class:`~discord.VoiceChannel`.
@@ -381,31 +461,10 @@ class VoiceChannelConverter(IDConverter[discord.VoiceChannel]):
.. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: Context, argument: str) -> discord.VoiceChannel:
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
result = None
guild = ctx.guild
return GuildChannelConverter._resolve_channel(ctx, argument, ctx.guild.voice_channels, discord.VoiceChannel)
if match is None:
# not a mention
if guild:
result = discord.utils.get(guild.voice_channels, name=argument)
else:
def check(c):
return isinstance(c, discord.VoiceChannel) and c.name == argument
result = discord.utils.find(check, bot.get_all_channels())
else:
channel_id = int(match.group(1))
if guild:
result = guild.get_channel(channel_id)
else:
result = _get_from_guilds(bot, 'get_channel', channel_id)
if not isinstance(result, discord.VoiceChannel):
raise ChannelNotFound(argument)
return result
class StageChannelConverter(IDConverter[discord.StageChannel]):
"""Converts to a :class:`~discord.StageChannel`.
@@ -421,31 +480,10 @@ class StageChannelConverter(IDConverter[discord.StageChannel]):
2. Lookup by mention.
3. Lookup by name
"""
async def convert(self, ctx: Context, argument: str) -> discord.StageChannel:
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
result = None
guild = ctx.guild
return GuildChannelConverter._resolve_channel(ctx, argument, ctx.guild.stage_channels, discord.StageChannel)
if match is None:
# not a mention
if guild:
result = discord.utils.get(guild.stage_channels, name=argument)
else:
def check(c):
return isinstance(c, discord.StageChannel) and c.name == argument
result = discord.utils.find(check, bot.get_all_channels())
else:
channel_id = int(match.group(1))
if guild:
result = guild.get_channel(channel_id)
else:
result = _get_from_guilds(bot, 'get_channel', channel_id)
if not isinstance(result, discord.StageChannel):
raise ChannelNotFound(argument)
return result
class CategoryChannelConverter(IDConverter[discord.CategoryChannel]):
"""Converts to a :class:`~discord.CategoryChannel`.
@@ -462,32 +500,10 @@ class CategoryChannelConverter(IDConverter[discord.CategoryChannel]):
.. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: Context, argument: str) -> discord.CategoryChannel:
bot = ctx.bot
return GuildChannelConverter._resolve_channel(ctx, argument, ctx.guild.categories, discord.CategoryChannel)
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
result = None
guild = ctx.guild
if match is None:
# not a mention
if guild:
result = discord.utils.get(guild.categories, name=argument)
else:
def check(c):
return isinstance(c, discord.CategoryChannel) and c.name == argument
result = discord.utils.find(check, bot.get_all_channels())
else:
channel_id = int(match.group(1))
if guild:
result = guild.get_channel(channel_id)
else:
result = _get_from_guilds(bot, 'get_channel', channel_id)
if not isinstance(result, discord.CategoryChannel):
raise ChannelNotFound(argument)
return result
class StoreChannelConverter(IDConverter[discord.StoreChannel]):
"""Converts to a :class:`~discord.StoreChannel`.
@@ -505,30 +521,8 @@ class StoreChannelConverter(IDConverter[discord.StoreChannel]):
"""
async def convert(self, ctx: Context, argument: str) -> discord.StoreChannel:
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
result = None
guild = ctx.guild
return GuildChannelConverter._resolve_channel(ctx, argument, ctx.guild.channels, discord.StoreChannel)
if match is None:
# not a mention
if guild:
result = discord.utils.get(guild.channels, name=argument)
else:
def check(c):
return isinstance(c, discord.StoreChannel) and c.name == argument
result = discord.utils.find(check, bot.get_all_channels())
else:
channel_id = int(match.group(1))
if guild:
result = guild.get_channel(channel_id)
else:
result = _get_from_guilds(bot, 'get_channel', channel_id)
if not isinstance(result, discord.StoreChannel):
raise ChannelNotFound(argument)
return result
class ColourConverter(Converter[discord.Colour]):
"""Converts to a :class:`~discord.Colour`.
@@ -542,7 +536,7 @@ class ColourConverter(Converter[discord.Colour]):
- ``#<hex>``
- ``0x#<hex>``
- ``rgb(<number>, <number>, <number>)``
- Any of the ``classmethod`` in :class:`Colour`
- Any of the ``classmethod`` in :class:`~discord.Colour`
- The ``_`` in the name can be optionally replaced with spaces.
@@ -612,13 +606,15 @@ class ColourConverter(Converter[discord.Colour]):
raise BadColourArgument(arg)
return method()
ColorConverter = ColourConverter
class RoleConverter(IDConverter[discord.Role]):
"""Converts to a :class:`~discord.Role`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
All lookups are via the local guild. If in a DM context, the converter raises
:exc:`.NoPrivateMessage` exception.
The lookup strategy is as follows (in order):
@@ -629,12 +625,13 @@ class RoleConverter(IDConverter[discord.Role]):
.. versionchanged:: 1.5
Raise :exc:`.RoleNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: Context, argument: str) -> discord.Role:
guild = ctx.guild
if not guild:
raise NoPrivateMessage()
match = self._get_id_match(argument) or re.match(r'<@&([0-9]+)>$', argument)
match = self._get_id_match(argument) or re.match(r'<@&([0-9]{15,20})>$', argument)
if match:
result = guild.get_role(int(match.group(1)))
else:
@@ -644,11 +641,14 @@ class RoleConverter(IDConverter[discord.Role]):
raise RoleNotFound(argument)
return result
class GameConverter(Converter[discord.Game]):
"""Converts to :class:`~discord.Game`."""
async def convert(self, ctx: Context, argument: str) -> discord.Game:
return discord.Game(name=argument)
class InviteConverter(Converter[discord.Invite]):
"""Converts to a :class:`~discord.Invite`.
@@ -657,6 +657,7 @@ class InviteConverter(Converter[discord.Invite]):
.. versionchanged:: 1.5
Raise :exc:`.BadInviteArgument` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: Context, argument: str) -> discord.Invite:
try:
invite = await ctx.bot.fetch_invite(argument)
@@ -664,6 +665,7 @@ class InviteConverter(Converter[discord.Invite]):
except Exception as exc:
raise BadInviteArgument() from exc
class GuildConverter(IDConverter[discord.Guild]):
"""Converts to a :class:`~discord.Guild`.
@@ -690,6 +692,7 @@ class GuildConverter(IDConverter[discord.Guild]):
raise GuildNotFound(argument)
return result
class EmojiConverter(IDConverter[discord.Emoji]):
"""Converts to a :class:`~discord.Emoji`.
@@ -705,8 +708,9 @@ class EmojiConverter(IDConverter[discord.Emoji]):
.. versionchanged:: 1.5
Raise :exc:`.EmojiNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: Context, argument: str) -> discord.Emoji:
match = self._get_id_match(argument) or re.match(r'<a?:[a-zA-Z0-9\_]+:([0-9]+)>$', argument)
match = self._get_id_match(argument) or re.match(r'<a?:[a-zA-Z0-9\_]{1,32}:([0-9]{15,20})>$', argument)
result = None
bot = ctx.bot
guild = ctx.guild
@@ -733,6 +737,7 @@ class EmojiConverter(IDConverter[discord.Emoji]):
return result
class PartialEmojiConverter(Converter[discord.PartialEmoji]):
"""Converts to a :class:`~discord.PartialEmoji`.
@@ -741,19 +746,22 @@ class PartialEmojiConverter(Converter[discord.PartialEmoji]):
.. versionchanged:: 1.5
Raise :exc:`.PartialEmojiConversionFailure` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: Context, argument: str) -> discord.PartialEmoji:
match = re.match(r'<(a?):([a-zA-Z0-9\_]+):([0-9]+)>$', argument)
match = re.match(r'<(a?):([a-zA-Z0-9\_]{1,32}):([0-9]{15,20})>$', argument)
if match:
emoji_animated = bool(match.group(1))
emoji_name = match.group(2)
emoji_id = int(match.group(3))
return discord.PartialEmoji.with_state(ctx.bot._connection, animated=emoji_animated, name=emoji_name,
id=emoji_id)
return discord.PartialEmoji.with_state(
ctx.bot._connection, animated=emoji_animated, name=emoji_name, id=emoji_id
)
raise PartialEmojiConversionFailure(argument)
class clean_content(Converter[str]):
"""Converts the argument to mention scrubbed version of
said content.
@@ -773,7 +781,8 @@ class clean_content(Converter[str]):
.. versionadded:: 1.7
"""
def __init__(self, *, fix_channel_mentions=False, use_nicknames=True, escape_markdown=False, remove_markdown=False):
def __init__(self, *, fix_channel_mentions: bool = False, use_nicknames: bool = True, escape_markdown: bool = False, remove_markdown: bool = False) -> None:
self.fix_channel_mentions = fix_channel_mentions
self.use_nicknames = use_nicknames
self.escape_markdown = escape_markdown
@@ -784,6 +793,7 @@ class clean_content(Converter[str]):
transformations = {}
if self.fix_channel_mentions and ctx.guild:
def resolve_channel(id, *, _get=ctx.guild.get_channel):
ch = _get(id)
return f'<#{id}>', ('#' + ch.name if ch else '#deleted-channel')
@@ -791,15 +801,18 @@ class clean_content(Converter[str]):
transformations.update(resolve_channel(channel) for channel in message.raw_channel_mentions)
if self.use_nicknames and ctx.guild:
def resolve_member(id, *, _get=ctx.guild.get_member):
m = _get(id)
return '@' + m.display_name if m else '@deleted-user'
else:
def resolve_member(id, *, _get=ctx.bot.get_user):
m = _get(id)
return '@' + m.name if m else '@deleted-user'
# fmt: off
transformations.update(
(f'<@{member_id}>', resolve_member(member_id))
for member_id in message.raw_mentions
@@ -809,16 +822,20 @@ class clean_content(Converter[str]):
(f'<@!{member_id}>', resolve_member(member_id))
for member_id in message.raw_mentions
)
# fmt: on
if ctx.guild:
def resolve_role(_id, *, _find=ctx.guild.get_role):
r = _find(_id)
return '@' + r.name if r else '@deleted-role'
# fmt: off
transformations.update(
(f'<@&{role_id}>', resolve_role(role_id))
for role_id in message.raw_role_mentions
)
# fmt: on
def repl(obj):
return transformations.get(obj.group(0), '')
@@ -834,28 +851,230 @@ class clean_content(Converter[str]):
# Completely ensure no mentions escape:
return discord.utils.escape_mentions(result)
class _Greedy:
class Greedy(List[T]):
r"""A special converter that greedily consumes arguments until it can't.
As a consequence of this behaviour, most input errors are silently discarded,
since it is used as an indicator of when to stop parsing.
When a parser error is met the greedy converter stops converting, undoes the
internal string parsing routine, and continues parsing regularly.
For example, in the following code:
.. code-block:: python3
@commands.command()
async def test(ctx, numbers: Greedy[int], reason: str):
await ctx.send("numbers: {}, reason: {}".format(numbers, reason))
An invocation of ``[p]test 1 2 3 4 5 6 hello`` would pass ``numbers`` with
``[1, 2, 3, 4, 5, 6]`` and ``reason`` with ``hello``\.
For more information, check :ref:`ext_commands_special_converters`.
"""
__slots__ = ('converter',)
def __init__(self, *, converter=None):
def __init__(self, *, converter: T):
self.converter = converter
def __getitem__(self, params):
def __repr__(self):
converter = getattr(self.converter, '__name__', repr(self.converter))
return f'Greedy[{converter}]'
def __class_getitem__(cls, params: Union[Tuple[T], T]) -> Greedy[T]:
if not isinstance(params, tuple):
params = (params,)
if len(params) != 1:
raise TypeError('Greedy[...] only takes a single argument')
converter = params[0]
if not (callable(converter) or isinstance(converter, Converter) or hasattr(converter, '__origin__')):
origin = getattr(converter, '__origin__', None)
args = getattr(converter, '__args__', ())
if not (callable(converter) or isinstance(converter, Converter) or origin is not None):
raise TypeError('Greedy[...] expects a type or a Converter instance.')
if converter is str or converter is type(None) or converter is _Greedy:
if converter in (str, type(None)) or origin is Greedy:
raise TypeError(f'Greedy[{converter.__name__}] is invalid.')
if getattr(converter, '__origin__', None) is Union and type(None) in converter.__args__:
if origin is Union and type(None) in args:
raise TypeError(f'Greedy[{converter!r}] is invalid.')
return self.__class__(converter=converter)
return cls(converter=converter)
Greedy = _Greedy()
def _convert_to_bool(argument: str) -> bool:
lowered = argument.lower()
if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'):
return True
elif lowered in ('no', 'n', 'false', 'f', '0', 'disable', 'off'):
return False
else:
raise BadBoolArgument(lowered)
def get_converter(param: inspect.Parameter) -> Any:
converter = param.annotation
if converter is param.empty:
if param.default is not param.empty:
converter = str if param.default is None else type(param.default)
else:
converter = str
return converter
_GenericAlias = type(List[T])
def is_generic_type(tp: Any, *, _GenericAlias: Type = _GenericAlias) -> bool:
return isinstance(tp, type) and issubclass(tp, Generic) or isinstance(tp, _GenericAlias) # type: ignore
CONVERTER_MAPPING: Dict[Type[Any], Any] = {
discord.Object: ObjectConverter,
discord.Member: MemberConverter,
discord.User: UserConverter,
discord.Message: MessageConverter,
discord.PartialMessage: PartialMessageConverter,
discord.TextChannel: TextChannelConverter,
discord.Invite: InviteConverter,
discord.Guild: GuildConverter,
discord.Role: RoleConverter,
discord.Game: GameConverter,
discord.Colour: ColourConverter,
discord.VoiceChannel: VoiceChannelConverter,
discord.StageChannel: StageChannelConverter,
discord.Emoji: EmojiConverter,
discord.PartialEmoji: PartialEmojiConverter,
discord.CategoryChannel: CategoryChannelConverter,
discord.StoreChannel: StoreChannelConverter,
discord.abc.GuildChannel: GuildChannelConverter,
}
async def _actual_conversion(ctx: Context, converter, argument: str, param: inspect.Parameter):
if converter is bool:
return _convert_to_bool(argument)
try:
module = converter.__module__
except AttributeError:
pass
else:
if module is not None and (module.startswith('discord.') and not module.endswith('converter')):
converter = CONVERTER_MAPPING.get(converter, converter)
try:
if inspect.isclass(converter) and issubclass(converter, Converter):
if inspect.ismethod(converter.convert):
return await converter.convert(ctx, argument)
else:
return await converter().convert(ctx, argument)
elif isinstance(converter, Converter):
return await converter.convert(ctx, argument)
except CommandError:
raise
except Exception as exc:
raise ConversionError(converter, exc) from exc
try:
return converter(argument)
except CommandError:
raise
except Exception as exc:
try:
name = converter.__name__
except AttributeError:
name = converter.__class__.__name__
raise BadArgument(f'Converting to "{name}" failed for parameter "{param.name}".') from exc
async def run_converters(ctx: Context, converter, argument: str, param: inspect.Parameter):
"""|coro|
Runs converters for a given converter, argument, and parameter.
This function does the same work that the library does under the hood.
.. versionadded:: 2.0
Parameters
------------
ctx: :class:`Context`
The invocation context to run the converters under.
converter: Any
The converter to run, this corresponds to the annotation in the function.
argument: :class:`str`
The argument to convert to.
param: :class:`inspect.Parameter`
The parameter being converted. This is mainly for error reporting.
Raises
-------
CommandError
The converter failed to convert.
Returns
--------
Any
The resulting conversion.
"""
origin = getattr(converter, '__origin__', None)
if origin is Union:
errors = []
_NoneType = type(None)
union_args = converter.__args__
for conv in union_args:
# if we got to this part in the code, then the previous conversions have failed
# so we should just undo the view, return the default, and allow parsing to continue
# with the other parameters
if conv is _NoneType and param.kind != param.VAR_POSITIONAL:
ctx.view.undo()
return None if param.default is param.empty else param.default
try:
value = await run_converters(ctx, conv, argument, param)
except CommandError as exc:
errors.append(exc)
else:
return value
# if we're here, then we failed all the converters
raise BadUnionArgument(param, union_args, errors)
if origin is Literal:
errors = []
conversions = {}
literal_args = converter.__args__
for literal in literal_args:
literal_type = type(literal)
try:
value = conversions[literal_type]
except KeyError:
try:
value = await _actual_conversion(ctx, literal_type, argument, param)
except CommandError as exc:
errors.append(exc)
conversions[literal_type] = object()
continue
else:
conversions[literal_type] = value
if value == literal:
return value
# if we're here, then we failed to match all the literals
raise BadLiteralArgument(param, literal_args, errors)
# This must be the last if-clause in the chain of origin checking
# Nearly every type is a generic type within the typing library
# So care must be taken to make sure a more specialised origin handle
# isn't overwritten by the widest if clause
if origin is not None and is_generic_type(converter):
converter = origin
return await _actual_conversion(ctx, converter, argument, param)

View File

@@ -34,6 +34,7 @@ __all__ = (
'BucketType',
'Cooldown',
'CooldownMapping',
'DynamicCooldownMapping',
'MaxConcurrency',
)
@@ -69,19 +70,15 @@ class BucketType(Enum):
class Cooldown:
__slots__ = ('rate', 'per', 'type', '_window', '_tokens', '_last')
__slots__ = ('rate', 'per', '_window', '_tokens', '_last')
def __init__(self, rate, per, type):
def __init__(self, rate, per):
self.rate = int(rate)
self.per = float(per)
self.type = type
self._window = 0.0
self._tokens = self.rate
self._last = 0.0
if not callable(self.type):
raise TypeError('Cooldown type must be a BucketType or callable')
def get_tokens(self, current=None):
if not current:
current = time.time()
@@ -128,18 +125,22 @@ class Cooldown:
self._last = 0.0
def copy(self):
return Cooldown(self.rate, self.per, self.type)
return Cooldown(self.rate, self.per)
def __repr__(self):
return '<Cooldown rate: {0.rate} per: {0.per} window: {0._window} tokens: {0._tokens}>'.format(self)
return f'<Cooldown rate: {self.rate} per: {self.per} window: {self._window} tokens: {self._tokens}>'
class CooldownMapping:
def __init__(self, original):
def __init__(self, original, type):
if not callable(type):
raise TypeError('Cooldown type must be a BucketType or callable')
self._cache = {}
self._cooldown = original
self._type = type
def copy(self):
ret = CooldownMapping(self._cooldown)
ret = CooldownMapping(self._cooldown, self._type)
ret._cache = self._cache.copy()
return ret
@@ -149,10 +150,10 @@ class CooldownMapping:
@classmethod
def from_cooldown(cls, rate, per, type):
return cls(Cooldown(rate, per, type))
return cls(Cooldown(rate, per), type)
def _bucket_key(self, msg):
return self._cooldown.type(msg)
return self._type(msg)
def _verify_cache_integrity(self, current=None):
# we want to delete all cache objects that haven't been used
@@ -163,15 +164,19 @@ class CooldownMapping:
for k in dead_keys:
del self._cache[k]
def create_bucket(self, message):
return self._cooldown.copy()
def get_bucket(self, message, current=None):
if self._cooldown.type is BucketType.default:
if self._type is BucketType.default:
return self._cooldown
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]
@@ -181,6 +186,24 @@ class CooldownMapping:
bucket = self.get_bucket(message, current)
return bucket.update_rate_limit(current)
class DynamicCooldownMapping(CooldownMapping):
def __init__(self, factory, type):
super().__init__(None, type)
self._factory = factory
def copy(self):
ret = DynamicCooldownMapping(self._factory, self._type)
ret._cache = self._cache.copy()
return ret
@property
def valid(self):
return True
def create_bucket(self, message):
return self._factory(message)
class _Semaphore:
"""This class is a version of a semaphore.
@@ -202,7 +225,7 @@ class _Semaphore:
self._waiters = deque()
def __repr__(self):
return '<_Semaphore value={0.value} waiters={1}>'.format(self, len(self._waiters))
return f'<_Semaphore value={self.value} waiters={len(self._waiters)}>'
def locked(self):
return self.value == 0
@@ -259,7 +282,7 @@ class MaxConcurrency:
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)
return f'<MaxConcurrency per={self.per!r} number={self.number} wait={self.wait}>'
def get_key(self, message):
return self.per.get_key(message)

View File

@@ -22,18 +22,24 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from typing import (
Any,
Callable,
Dict,
Literal,
Union,
)
import asyncio
import functools
import inspect
import typing
import datetime
import sys
import types
import discord
from .errors import *
from .cooldowns import Cooldown, BucketType, CooldownMapping, MaxConcurrency
from . import converter as converters
from .cooldowns import Cooldown, BucketType, CooldownMapping, MaxConcurrency, DynamicCooldownMapping
from .converter import run_converters, get_converter, Greedy
from ._types import _BaseCommand
from .cog import Cog
@@ -54,6 +60,7 @@ __all__ = (
'bot_has_permissions',
'bot_has_any_role',
'cooldown',
'dynamic_cooldown',
'max_concurrency',
'dm_only',
'guild_only',
@@ -63,6 +70,40 @@ __all__ = (
'bot_has_guild_permissions'
)
def unwrap_function(function: Callable[..., Any]) -> Callable[..., Any]:
partial = functools.partial
while True:
if hasattr(function, '__wrapped__'):
function = function.__wrapped__
elif isinstance(function, partial):
function = function.func
else:
return function
def get_signature_parameters(function: Callable[..., Any], globalns: Dict[str, Any]) -> Dict[str, inspect.Parameter]:
signature = inspect.signature(function)
params = {}
cache: Dict[str, Any] = {}
eval_annotation = discord.utils.evaluate_annotation
for name, parameter in signature.parameters.items():
annotation = parameter.annotation
if annotation is parameter.empty:
params[name] = parameter
continue
if annotation is None:
params[name] = parameter.replace(annotation=type(None))
continue
annotation = eval_annotation(annotation, globalns, globalns, cache)
if annotation is Greedy:
raise TypeError('Unparameterized Greedy[...] is disallowed in signature.')
params[name] = parameter.replace(annotation=annotation)
return params
def wrap_callback(coro):
@functools.wraps(coro)
async def wrapped(*args, **kwargs):
@@ -99,14 +140,6 @@ def hooked_wrapped_callback(command, ctx, coro):
return ret
return wrapped
def _convert_to_bool(argument):
lowered = argument.lower()
if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'):
return True
elif lowered in ('no', 'n', 'false', 'f', '0', 'disable', 'off'):
return False
else:
raise BadBoolArgument(lowered)
class _CaseInsensitiveDict(dict):
def __contains__(self, k):
@@ -152,8 +185,8 @@ class Command(_BaseCommand):
If the command is invoked while it is disabled, then
:exc:`.DisabledCommand` is raised to the :func:`.on_command_error`
event. Defaults to ``True``.
parent: Optional[:class:`Command`]
The parent command that this command belongs to. ``None`` if there
parent: Optional[:class:`Group`]
The parent group that this command belongs to. ``None`` if there
isn't one.
cog: Optional[:class:`Cog`]
The cog that this command belongs to. ``None`` if there isn't one.
@@ -193,6 +226,14 @@ class Command(_BaseCommand):
If ``True``\, cooldown processing is done after argument parsing,
which calls converters. If ``False`` then cooldown processing is done
first and then the converters are called second. Defaults to ``False``.
extras: :class:`dict`
A dict of user provided extras to attach to the Command.
.. note::
This object may be copied by the library.
.. versionadded:: 2.0
"""
def __new__(cls, *args, **kwargs):
@@ -236,6 +277,7 @@ class Command(_BaseCommand):
self.usage = kwargs.get('usage')
self.rest_is_raw = kwargs.get('rest_is_raw', False)
self.aliases = kwargs.get('aliases', [])
self.extras = kwargs.get('extras', {})
if not isinstance(self.aliases, (list, tuple)):
raise TypeError("Aliases of a command must be a list or a tuple of strings.")
@@ -256,7 +298,10 @@ class Command(_BaseCommand):
except AttributeError:
cooldown = kwargs.get('cooldown')
finally:
self._buckets = CooldownMapping(cooldown)
if cooldown is None:
self._buckets = CooldownMapping(cooldown, BucketType.default)
elif isinstance(cooldown, CooldownMapping):
self._buckets = cooldown
try:
max_concurrency = func.__commands_max_concurrency__
@@ -295,41 +340,15 @@ class Command(_BaseCommand):
@callback.setter
def callback(self, function):
self._callback = function
self.module = function.__module__
signature = inspect.signature(function)
self.params = signature.parameters.copy()
# see: https://bugs.python.org/issue41341
resolve = self._recursive_resolve if sys.version_info < (3, 9) else self._return_resolved
unwrap = unwrap_function(function)
self.module = unwrap.__module__
try:
type_hints = {k: resolve(v) for k, v in typing.get_type_hints(function).items()}
except NameError as e:
raise NameError(f'unresolved forward reference: {e.args[0]}') from None
globalns = unwrap.__globals__
except AttributeError:
globalns = {}
for key, value in self.params.items():
# coalesce the forward references
if key in type_hints:
self.params[key] = value = value.replace(annotation=type_hints[key])
# fail early for when someone passes an unparameterized Greedy type
if value.annotation is converters.Greedy:
raise TypeError('Unparameterized Greedy[...] is disallowed in signature.')
def _return_resolved(self, type, **kwargs):
return type
def _recursive_resolve(self, type, *, globals=None):
if not isinstance(type, typing.ForwardRef):
return type
resolved = eval(type.__forward_arg__, globals)
args = typing.get_args(resolved)
for index, arg in enumerate(args):
inner_resolve_result = self._recursive_resolve(arg, globals=globals)
resolved[index] = inner_resolve_result
return resolved
self.params = get_signature_parameters(function, globalns)
def add_check(self, func):
"""Adds a check to the command.
@@ -451,92 +470,17 @@ class Command(_BaseCommand):
finally:
ctx.bot.dispatch('command_error', ctx, error)
async def _actual_conversion(self, ctx, converter, argument, param):
if converter is bool:
return _convert_to_bool(argument)
try:
module = converter.__module__
except AttributeError:
pass
else:
if module is not None and (module.startswith('discord.') and not module.endswith('converter')):
converter = getattr(converters, converter.__name__ + 'Converter', converter)
try:
if inspect.isclass(converter) and issubclass(converter, converters.Converter):
if inspect.ismethod(converter.convert):
return await converter.convert(ctx, argument)
else:
return await converter().convert(ctx, argument)
elif isinstance(converter, converters.Converter):
return await converter.convert(ctx, argument)
except CommandError:
raise
except Exception as exc:
raise ConversionError(converter, exc) from exc
try:
return converter(argument)
except CommandError:
raise
except Exception as exc:
try:
name = converter.__name__
except AttributeError:
name = converter.__class__.__name__
raise BadArgument(f'Converting to "{name}" failed for parameter "{param.name}".') from exc
async def do_conversion(self, ctx, converter, argument, param):
try:
origin = converter.__origin__
except AttributeError:
pass
else:
if origin is typing.Union:
errors = []
_NoneType = type(None)
for conv in converter.__args__:
# if we got to this part in the code, then the previous conversions have failed
# so we should just undo the view, return the default, and allow parsing to continue
# with the other parameters
if conv is _NoneType and param.kind != param.VAR_POSITIONAL:
ctx.view.undo()
return None if param.default is param.empty else param.default
try:
value = await self._actual_conversion(ctx, conv, argument, param)
except CommandError as exc:
errors.append(exc)
else:
return value
# if we're here, then we failed all the converters
raise BadUnionArgument(param, converter.__args__, errors)
return await self._actual_conversion(ctx, converter, argument, param)
def _get_converter(self, param):
converter = param.annotation
if converter is param.empty:
if param.default is not param.empty:
converter = str if param.default is None else type(param.default)
else:
converter = str
return converter
async def transform(self, ctx, param):
required = param.default is param.empty
converter = self._get_converter(param)
converter = get_converter(param)
consume_rest_is_special = param.kind == param.KEYWORD_ONLY and not self.rest_is_raw
view = ctx.view
view.skip_ws()
# The greedy converter is simple -- it keeps going until it fails in which case,
# it undos the view ready for the next parameter to use instead
if type(converter) is converters._Greedy:
if param.kind == param.POSITIONAL_OR_KEYWORD or param.kind == param.POSITIONAL_ONLY:
if isinstance(converter, Greedy):
if param.kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY):
return await self._transform_greedy_pos(ctx, param, required, converter.converter)
elif param.kind == param.VAR_POSITIONAL:
return await self._transform_greedy_var_pos(ctx, param, converter.converter)
@@ -552,6 +496,8 @@ class Command(_BaseCommand):
if required:
if self._is_typing_optional(param.annotation):
return None
if hasattr(converter, '__commands_is_flag__') and converter._can_be_constructible():
return await converter._construct_default(ctx)
raise MissingRequiredArgument(param)
return param.default
@@ -562,7 +508,7 @@ class Command(_BaseCommand):
argument = view.get_quoted_word()
view.previous = previous
return await self.do_conversion(ctx, converter, argument, param)
return await run_converters(ctx, converter, argument, param)
async def _transform_greedy_pos(self, ctx, param, required, converter):
view = ctx.view
@@ -574,7 +520,7 @@ class Command(_BaseCommand):
view.skip_ws()
try:
argument = view.get_quoted_word()
value = await self.do_conversion(ctx, converter, argument, param)
value = await run_converters(ctx, converter, argument, param)
except (CommandError, ArgumentParsingError):
view.index = previous
break
@@ -590,7 +536,7 @@ class Command(_BaseCommand):
previous = view.index
try:
argument = view.get_quoted_word()
value = await self.do_conversion(ctx, converter, argument, param)
value = await run_converters(ctx, converter, argument, param)
except (CommandError, ArgumentParsingError):
view.index = previous
raise RuntimeError() from None # break loop
@@ -598,22 +544,25 @@ class Command(_BaseCommand):
return value
@property
def clean_params(self):
"""OrderedDict[:class:`str`, :class:`inspect.Parameter`]:
Retrieves the parameter OrderedDict without the context or self parameters.
def clean_params(self) -> Dict[str, inspect.Parameter]:
"""Dict[:class:`str`, :class:`inspect.Parameter`]:
Retrieves the parameter dictionary without the context or self parameters.
Useful for inspecting signature.
"""
result = self.params.copy()
if self.cog is not None:
# first parameter is self
result.popitem(last=False)
try:
del result[next(iter(result))]
except StopIteration:
raise ValueError("missing 'self' parameter") from None
try:
# first/second parameter is context
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
return result
@@ -634,7 +583,7 @@ class Command(_BaseCommand):
@property
def parents(self):
"""List[:class:`Command`]: Retrieves the parents of this command.
"""List[:class:`Group`]: Retrieves the parents of this command.
If the command has no parents then it returns an empty :class:`list`.
@@ -652,7 +601,7 @@ class Command(_BaseCommand):
@property
def root_parent(self):
"""Optional[:class:`Command`]: Retrieves the root parent of this command.
"""Optional[:class:`Group`]: Retrieves the root parent of this command.
If the command has no parents then it returns ``None``.
@@ -695,26 +644,25 @@ class Command(_BaseCommand):
try:
next(iterator)
except StopIteration:
fmt = 'Callback for {0.name} command is missing "self" parameter.'
raise discord.ClientException(fmt.format(self))
raise discord.ClientException(f'Callback for {self.name} command is missing "self" parameter.')
# next we have the 'ctx' as the next parameter
try:
next(iterator)
except StopIteration:
fmt = 'Callback for {0.name} command is missing "ctx" parameter.'
raise discord.ClientException(fmt.format(self))
raise discord.ClientException(f'Callback for {self.name} command is missing "ctx" parameter.')
for name, param in iterator:
if param.kind == param.POSITIONAL_OR_KEYWORD or param.kind == param.POSITIONAL_ONLY:
ctx.current_parameter = param
if param.kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY):
transformed = await self.transform(ctx, param)
args.append(transformed)
elif param.kind == param.KEYWORD_ONLY:
# kwarg only param denotes "consume rest" semantics
if self.rest_is_raw:
converter = self._get_converter(param)
converter = get_converter(param)
argument = view.read_rest()
kwargs[name] = await self.do_conversion(ctx, converter, argument, param)
kwargs[name] = await run_converters(ctx, converter, argument, param)
else:
kwargs[name] = await self.transform(ctx, param)
break
@@ -780,9 +728,10 @@ class Command(_BaseCommand):
dt = ctx.message.edited_at or ctx.message.created_at
current = dt.replace(tzinfo=datetime.timezone.utc).timestamp()
bucket = self._buckets.get_bucket(ctx.message, current)
retry_after = bucket.update_rate_limit(current)
if retry_after:
raise CommandOnCooldown(bucket, retry_after)
if bucket is not None:
retry_after = bucket.update_rate_limit(current)
if retry_after:
raise CommandOnCooldown(bucket, retry_after)
async def prepare(self, ctx):
ctx.command = self
@@ -986,9 +935,9 @@ class Command(_BaseCommand):
def short_doc(self):
""":class:`str`: Gets the "short" documentation of a command.
By default, this is the :attr:`brief` attribute.
By default, this is the :attr:`.brief` attribute.
If that lookup leads to an empty string then the first line of the
:attr:`help` attribute is used instead.
:attr:`.help` attribute is used instead.
"""
if self.brief is not None:
return self.brief
@@ -997,15 +946,7 @@ class Command(_BaseCommand):
return ''
def _is_typing_optional(self, annotation):
try:
origin = annotation.__origin__
except AttributeError:
return False
if origin is not typing.Union:
return False
return annotation.__args__[-1] is type(None)
return getattr(annotation, '__origin__', None) is Union and type(None) in annotation.__args__
@property
def signature(self):
@@ -1013,15 +954,29 @@ class Command(_BaseCommand):
if self.usage is not None:
return self.usage
params = self.clean_params
if not params:
return ''
result = []
for name, param in params.items():
greedy = isinstance(param.annotation, converters._Greedy)
greedy = isinstance(param.annotation, Greedy)
optional = False # postpone evaluation of if it's an optional argument
# for typing.Literal[...], typing.Optional[typing.Literal[...]], and Greedy[typing.Literal[...]], the
# parameter signature is a literal list of it's values
annotation = param.annotation.converter if greedy else param.annotation
origin = getattr(annotation, '__origin__', None)
if not greedy and origin is Union:
none_cls = type(None)
union_args = annotation.__args__
optional = union_args[-1] is none_cls
if len(union_args) == 2 and optional:
annotation = union_args[0]
origin = getattr(annotation, '__origin__', None)
if origin is Literal:
name = '|'.join(f'"{v}"' if isinstance(v, str) else str(v) for v in annotation.__args__)
if param.default is not param.empty:
# We don't want None or '' to trigger the [name=value] case and instead it should
# do [name] since [name=None] or [name=] are not exactly useful for the user.
@@ -1040,7 +995,7 @@ class Command(_BaseCommand):
result.append(f'[{name}...]')
elif greedy:
result.append(f'[{name}]...')
elif self._is_typing_optional(param.annotation):
elif optional:
result.append(f'[{name}]')
else:
result.append(f'<{name}>')
@@ -1051,7 +1006,7 @@ class Command(_BaseCommand):
"""|coro|
Checks if the command can be executed by checking all the predicates
inside the :attr:`checks` attribute. This also checks whether the
inside the :attr:`~Command.checks` attribute. This also checks whether the
command is disabled.
.. versionchanged:: 1.3
@@ -1981,9 +1936,48 @@ def cooldown(rate, per, type=BucketType.default):
def decorator(func):
if isinstance(func, Command):
func._buckets = CooldownMapping(Cooldown(rate, per, type))
func._buckets = CooldownMapping(Cooldown(rate, per), type)
else:
func.__commands_cooldown__ = Cooldown(rate, per, type)
func.__commands_cooldown__ = CooldownMapping(Cooldown(rate, per), type)
return func
return decorator
def dynamic_cooldown(cooldown, type=BucketType.default):
"""A decorator that adds a dynamic cooldown to a :class:`.Command`
This differs from :func:`.cooldown` in that it takes a function that
accepts a single parameter of type :class:`.discord.Message` and must
return a :class:`.Cooldown`
A cooldown allows a command to only be used a specific amount
of times in a specific time frame. These cooldowns can be based
either on a per-guild, per-channel, per-user, per-role or global basis.
Denoted by the third argument of ``type`` which must be of enum
type :class:`.BucketType`.
If a cooldown is triggered, then :exc:`.CommandOnCooldown` is triggered in
:func:`.on_command_error` and the local error handler.
A command can only have a single cooldown.
.. versionadded:: 2.0
Parameters
------------
cooldown: Callable[[:class:`.discord.Message`], :class:`.Cooldown`]
A function that takes a message and returns a cooldown that will
apply to this invocation
type: :class:`.BucketType`
The type of cooldown to have.
"""
if not callable(cooldown):
raise TypeError("A callable must be provided")
def decorator(func):
if isinstance(func, Command):
func._buckets = DynamicCooldownMapping(cooldown, type)
else:
func.__commands_cooldown__ = DynamicCooldownMapping(cooldown, type)
return func
return decorator
@@ -2046,7 +2040,7 @@ def before_invoke(coro):
@commands.before_invoke(record_usage)
@commands.command()
async def when(self, ctx): # Output: <User> used when at <Time>
await ctx.send('and i have existed since {}'.format(ctx.bot.user.created_at))
await ctx.send(f'and i have existed since {ctx.bot.user.created_at}')
@commands.command()
async def where(self, ctx): # Output: <Nothing>

View File

@@ -42,12 +42,14 @@ __all__ = (
'MaxConcurrencyReached',
'NotOwner',
'MessageNotFound',
'ObjectNotFound',
'MemberNotFound',
'GuildNotFound',
'UserNotFound',
'ChannelNotFound',
'ChannelNotReadable',
'BadColourArgument',
'BadColorArgument',
'RoleNotFound',
'BadInviteArgument',
'EmojiNotFound',
@@ -62,6 +64,7 @@ __all__ = (
'NSFWChannelRequired',
'ConversionError',
'BadUnionArgument',
'BadLiteralArgument',
'ArgumentParsingError',
'UnexpectedQuoteError',
'InvalidEndOfQuotedStringError',
@@ -73,6 +76,11 @@ __all__ = (
'ExtensionFailed',
'ExtensionNotFound',
'CommandRegistrationError',
'FlagError',
'BadFlagArgument',
'MissingFlagArgument',
'TooManyFlags',
'MissingRequiredFlag',
)
class CommandError(DiscordException):
@@ -82,7 +90,7 @@ class CommandError(DiscordException):
This exception and exceptions inherited from it are handled
in a special way as they are caught and passed into a special event
from :class:`.Bot`\, :func:`on_command_error`.
from :class:`.Bot`\, :func:`.on_command_error`.
"""
def __init__(self, message=None, *args):
if message is not None:
@@ -212,6 +220,23 @@ class NotOwner(CheckFailure):
"""
pass
class ObjectNotFound(BadArgument):
"""Exception raised when the argument provided did not match the format
of an ID or a mention.
This inherits from :exc:`BadArgument`
.. versionadded:: 2.0
Attributes
-----------
argument: :class:`str`
The argument supplied by the caller that was not matched
"""
def __init__(self, argument):
self.argument = argument
super().__init__(f'{argument!r} does not follow a valid ID or mention format.')
class MemberNotFound(BadArgument):
"""Exception raised when the member provided was not found in the bot's
cache.
@@ -424,7 +449,7 @@ class CommandInvokeError(CommandError):
"""
def __init__(self, e):
self.original = e
super().__init__('Command raised an exception: {0.__class__.__name__}: {0}'.format(e))
super().__init__(f'Command raised an exception: {e.__class__.__name__}: {e}')
class CommandOnCooldown(CommandError):
"""Exception raised when the command being invoked is on cooldown.
@@ -433,7 +458,7 @@ class CommandOnCooldown(CommandError):
Attributes
-----------
cooldown: Cooldown
cooldown: ``Cooldown``
A class with attributes ``rate``, ``per``, and ``type`` similar to
the :func:`.cooldown` decorator.
retry_after: :class:`float`
@@ -464,7 +489,7 @@ class MaxConcurrencyReached(CommandError):
suffix = 'per %s' % name if per.name != 'default' else 'globally'
plural = '%s times %s' if number > 1 else '%s time %s'
fmt = plural % (number, suffix)
super().__init__(f'Too many people using this command. It can only be used {fmt} concurrently.')
super().__init__(f'Too many people are using this command. It can only be used {fmt} concurrently.')
class MissingRole(CheckFailure):
"""Exception raised when the command invoker lacks a role to run a command.
@@ -630,7 +655,7 @@ class BadUnionArgument(UserInputError):
-----------
param: :class:`inspect.Parameter`
The parameter that failed being converted.
converters: Tuple[Type, ...]
converters: Tuple[Type, ``...``]
A tuple of converters attempted in conversion, in order of failure.
errors: List[:class:`CommandError`]
A list of errors that were caught from failing the conversion.
@@ -644,6 +669,8 @@ class BadUnionArgument(UserInputError):
try:
return x.__name__
except AttributeError:
if hasattr(x, '__origin__'):
return repr(x)
return x.__class__.__name__
to_string = [_get_name(x) for x in converters]
@@ -654,6 +681,36 @@ class BadUnionArgument(UserInputError):
super().__init__(f'Could not convert "{param.name}" into {fmt}.')
class BadLiteralArgument(UserInputError):
"""Exception raised when a :data:`typing.Literal` converter fails for all
its associated values.
This inherits from :exc:`UserInputError`
.. versionadded:: 2.0
Attributes
-----------
param: :class:`inspect.Parameter`
The parameter that failed being converted.
literals: Tuple[Any, ``...``]
A tuple of values compared against in conversion, in order of failure.
errors: List[:class:`CommandError`]
A list of errors that were caught from failing the conversion.
"""
def __init__(self, param, literals, errors):
self.param = param
self.literals = literals
self.errors = errors
to_string = [repr(l) for l in literals]
if len(to_string) > 2:
fmt = '{}, or {}'.format(', '.join(to_string[:-1]), to_string[-1])
else:
fmt = ' or '.join(to_string)
super().__init__(f'Could not convert "{param.name}" into the literal {fmt}.')
class ArgumentParsingError(UserInputError):
"""An exception raised when the parser fails to parse a user's input.
@@ -764,8 +821,8 @@ class ExtensionFailed(ExtensionError):
"""
def __init__(self, name, original):
self.original = original
fmt = 'Extension {0!r} raised an error: {1.__class__.__name__}: {1}'
super().__init__(fmt.format(name, original), name=name)
msg = f'Extension {name!r} raised an error: {original.__class__.__name__}: {original}'
super().__init__(msg, name=name)
class ExtensionNotFound(ExtensionError):
"""An exception raised when an extension is not found.
@@ -779,13 +836,10 @@ class ExtensionNotFound(ExtensionError):
-----------
name: :class:`str`
The extension that had the error.
original: :class:`NoneType`
Always ``None`` for backwards compatibility.
"""
def __init__(self, name, original=None):
self.original = None
fmt = 'Extension {0!r} could not be loaded.'
super().__init__(fmt.format(name), name=name)
def __init__(self, name):
msg = f'Extension {name!r} could not be loaded.'
super().__init__(msg, name=name)
class CommandRegistrationError(ClientException):
"""An exception raised when the command can't be added
@@ -807,3 +861,76 @@ class CommandRegistrationError(ClientException):
self.alias_conflict = alias_conflict
type_ = 'alias' if alias_conflict else 'command'
super().__init__(f'The {type_} {name} is already an existing command or alias.')
class FlagError(BadArgument):
"""The base exception type for all flag parsing related errors.
This inherits from :exc:`BadArgument`.
.. versionadded:: 2.0
"""
pass
class TooManyFlags(FlagError):
"""An exception raised when a flag has received too many values.
This inherits from :exc:`FlagError`.
.. versionadded:: 2.0
Attributes
------------
flag: :class:`~discord.ext.commands.Flag`
The flag that received too many values.
values: List[:class:`str`]
The values that were passed.
"""
def __init__(self, flag, values):
self.flag = flag
self.values = values
super().__init__(f'Too many flag values, expected {flag.max_args} but received {len(values)}.')
class BadFlagArgument(FlagError):
"""An exception raised when a flag failed to convert a value.
"""
def __init__(self, flag):
self.flag = flag
try:
name = flag.annotation.__name__
except AttributeError:
name = flag.annotation.__class__.__name__
super().__init__(f'Could not convert to {name!r} for flag {flag.name!r}')
class MissingRequiredFlag(FlagError):
"""An exception raised when a required flag was not given.
This inherits from :exc:`FlagError`
.. versionadded:: 2.0
Attributes
-----------
flag: :class:`~discord.ext.commands.Flag`
The required flag that was not found.
"""
def __init__(self, flag):
self.flag = flag
super().__init__(f'Flag {flag.name!r} is required and missing')
class MissingFlagArgument(FlagError):
"""An exception raised when a flag did not get a value.
This inherits from :exc:`FlagError`
.. versionadded:: 2.0
Attributes
-----------
flag: :class:`~discord.ext.commands.Flag`
The flag that did not get a value.
"""
def __init__(self, flag):
self.flag = flag
super().__init__(f'Flag {flag.name!r} does not have an argument')

View File

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

@@ -60,6 +60,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,6 +82,7 @@ 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'):
self.prefix = prefix
self.suffix = suffix
@@ -92,7 +94,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
@@ -150,7 +152,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
@@ -171,10 +173,12 @@ class Paginator:
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)
@@ -212,8 +216,8 @@ class _HelpCommandImpl(Command):
def clean_params(self):
result = self.params.copy()
try:
result.popitem(last=False)
except Exception:
del result[next(iter(result))]
except StopIteration:
raise ValueError('Missing context parameter') from None
else:
return result
@@ -250,6 +254,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 +277,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
@@ -288,7 +293,7 @@ class HelpCommand:
'@everyone': '@\u200beveryone',
'@here': '@\u200bhere',
r'<@!?[0-9]{17,22}>': '@deleted-user',
r'<@&[0-9]{17,22}>': '@deleted-role'
r'<@&[0-9]{17,22}>': '@deleted-role',
}
MENTION_PATTERN = re.compile('|'.join(MENTION_TRANSFORMS.keys()))
@@ -305,10 +310,7 @@ 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
@@ -369,25 +371,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
@@ -442,7 +429,7 @@ class HelpCommand:
else:
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.
@@ -607,10 +594,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):
@@ -631,8 +615,7 @@ class HelpCommand:
"""|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.
@@ -880,6 +863,7 @@ class HelpCommand:
else:
return await self.send_command_help(cmd)
class DefaultHelpCommand(HelpCommand):
"""The implementation of the default help command.
@@ -934,14 +918,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 +948,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.
"""
@@ -1030,6 +1016,7 @@ class DefaultHelpCommand(HelpCommand):
self.paginator.add_line(bot.description, empty=True)
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
@@ -1083,6 +1070,7 @@ class DefaultHelpCommand(HelpCommand):
await self.send_pages()
class MinimalHelpCommand(HelpCommand):
"""An implementation of a help command with minimal output.
@@ -1149,11 +1137,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.
@@ -1202,7 +1192,7 @@ class MinimalHelpCommand(HelpCommand):
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))
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.
@@ -1273,6 +1263,7 @@ class MinimalHelpCommand(HelpCommand):
self.paginator.add_line(note, empty=True)
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

View File

@@ -189,4 +189,4 @@ class StringView:
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,8 +22,25 @@ 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,
Coroutine,
Generic,
List,
Optional,
TYPE_CHECKING,
Type,
TypeVar,
Union,
cast,
)
import aiohttp
import discord
import inspect
@@ -31,7 +48,16 @@ import logging
import sys
import traceback
from collections.abc import Sequence
from discord.backoff import ExponentialBackoff
from discord.utils import MISSING
if TYPE_CHECKING:
from typing_extensions import Concatenate, ParamSpec
P = ParamSpec("P")
else:
P = TypeVar("P") # hacky runtime fix
log = logging.getLogger(__name__)
@@ -39,19 +65,63 @@ __all__ = (
'loop',
)
class Loop:
C = TypeVar('C')
T = TypeVar('T')
_coro = Coroutine[Any, Any, T]
_func = Callable[..., Awaitable[Any]]
FT = TypeVar('FT', bound=_func)
ET = TypeVar('ET', bound=Callable[[Any, BaseException], Awaitable[Any]])
LT = TypeVar('LT', bound='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:
return self.future
def done(self) -> bool:
return self.future.done()
def cancel(self) -> None:
self.handle.cancel()
self.future.cancel()
class Loop(Generic[C, P, T]):
"""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: Callable[P, _coro[T]],
seconds: float,
hours: float,
minutes: float,
time: Union[datetime.time, Sequence[datetime.time]],
count: Optional[int],
reconnect: bool,
loop: Optional[asyncio.AbstractEventLoop],
) -> None:
self.coro: Callable[P, _coro[T]] = coro
self.reconnect: bool = reconnect
self.loop: Optional[asyncio.AbstractEventLoop] = loop
self.count: Optional[int] = count
self._current_loop = 0
self._handle = None
self._task = None
self._injected = None
self._injected: Optional[C] = None
self._valid_exception = (
OSError,
discord.GatewayNotFound,
@@ -69,15 +139,15 @@ class Loop:
if self.count is not None and self.count <= 0:
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._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):
async def _call_loop_function(self, name: str, *args: Any, **kwargs: Any) -> None:
coro = getattr(self, '_' + name)
if coro is None:
return
@@ -87,14 +157,23 @@ 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) # type: ignore
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
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,22 +181,27 @@ 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
@@ -127,17 +211,26 @@ class Loop:
raise exc
finally:
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: C, objtype: Type[C]) -> Loop[C, P, T]:
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[C, P, T](
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,12 +239,52 @@ 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
@@ -162,7 +295,7 @@ class Loop:
return None
return self._next_iteration
async def __call__(self, *args, **kwargs):
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
r"""|coro|
Calls the internal callback that the task holds.
@@ -182,7 +315,7 @@ class Loop:
return await self.coro(*args, **kwargs)
def start(self, *args, **kwargs):
def start(self, *args: P.args, **kwargs: P.kwargs) -> asyncio.Task:
r"""Starts the internal task in the event loop.
Parameters
@@ -215,7 +348,7 @@ class 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
@@ -236,15 +369,15 @@ class Loop:
if self._task 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: P.args, **kwargs: P.kwargs) -> None:
r"""A convenience method to restart the internal task.
.. note::
@@ -268,7 +401,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
@@ -297,7 +430,7 @@ class Loop:
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 +439,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 +456,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]:
"""Optional[:class:`asyncio.Task`]: Fetches the internal task or ``None`` if there isn't one running."""
return self._task
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
async def _error(self, *args):
exception = args[-1]
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 +503,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 +531,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 +557,82 @@ 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 = cast(datetime.datetime, 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: Optional[datetime.datetime] = None) -> 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 or 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):
ret = time if time.tzinfo is not None else time.replace(tzinfo=utc)
return [ret]
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 = []
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 +643,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: Optional[asyncio.AbstractEventLoop] = None,
) -> Callable[[Union[Callable[Concatenate[Type[C], P], _coro[T]], Callable[P, _coro[T]]]], Loop[C, P, T]]:
"""A decorator that schedules a task in the background for you with
optional reconnect logic. The decorator returns a :class:`Loop`.
@@ -478,6 +714,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.
@@ -485,7 +734,7 @@ def loop(*, seconds=0, minutes=0, hours=0, count=None, reconnect=True, loop=None
Whether to handle errors and restart the task
using an exponential back-off algorithm similar to the
one used in :meth:`discord.Client.connect`.
loop: :class:`asyncio.AbstractEventLoop`
loop: Optional[:class:`asyncio.AbstractEventLoop`]
The loop to use to register the task, if not given
defaults to :func:`asyncio.get_event_loop`.
@@ -494,16 +743,18 @@ 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):
def decorator(func: Union[Callable[Concatenate[Type[C], P], _coro[T]], Callable[P, _coro[T]]]) -> Loop[C, P, T]:
kwargs = {
'seconds': seconds,
'minutes': minutes,
'hours': hours,
'count': count,
'time': time,
'reconnect': reconnect,
'loop': loop
'loop': loop,
}
return Loop(func, **kwargs)
return Loop[C, P, T](func, **kwargs)
return decorator

View File

@@ -22,13 +22,17 @@ 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',
)
class File:
r"""A parameter object used for :meth:`abc.Messageable.send`
for sending file objects.
@@ -40,7 +44,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.
@@ -62,9 +66,18 @@ class File:
__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')
@@ -96,7 +109,7 @@ class File:
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 +121,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

@@ -34,11 +34,13 @@ __all__ = (
'PublicUserFlags',
'Intents',
'MemberCacheFlags',
'ApplicationFlags',
)
FV = TypeVar('FV', bound='flag_value')
BF = TypeVar('BF', bound='BaseFlags')
class flag_value(Generic[BF]):
def __init__(self, func: Callable[[Any], int]):
self.flag = func(None)
@@ -63,16 +65,20 @@ class flag_value(Generic[BF]):
def __repr__(self):
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]]
@@ -136,6 +144,7 @@ class BaseFlags:
else:
raise TypeError(f'Value to set for {self.__class__.__name__} must be a bool.')
@fill_with_flags(inverted=True)
class SystemChannelFlags(BaseFlags):
r"""Wraps up a Discord system channel flag value.
@@ -199,6 +208,14 @@ class SystemChannelFlags(BaseFlags):
""":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,14 @@ 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
@fill_with_flags()
class PublicUserFlags(BaseFlags):
r"""Wraps up the Discord User Public flags.
@@ -368,6 +393,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)]
@@ -488,6 +521,7 @@ class Intents(BaseFlags):
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 +530,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>`.
@@ -545,6 +579,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.
"""
@@ -627,7 +664,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 +718,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 +837,7 @@ class Intents(BaseFlags):
"""
return 1 << 14
@fill_with_flags()
class MemberCacheFlags(BaseFlags):
"""Controls the library's cache policy when it comes to members.
@@ -875,17 +911,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 +919,7 @@ class MemberCacheFlags(BaseFlags):
Members that leave voice are no longer cached.
"""
return 2
return 1
@flag_value
def joined(self):
@@ -905,7 +930,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 +951,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')
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)
@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

View File

@@ -373,7 +373,6 @@ class DiscordWebSocket:
},
'compress': True,
'large_threshold': 250,
'guild_subscriptions': self._connection.guild_subscriptions,
'v': 3
}
}
@@ -600,7 +599,9 @@ class DiscordWebSocket:
if activity is not None:
if not isinstance(activity, BaseActivity):
raise InvalidArgument('activity must derive from BaseActivity.')
activity = activity.to_dict()
activity = [activity.to_dict()]
else:
activity = []
if status == 'idle':
since = int(time.time() * 1000)
@@ -608,7 +609,7 @@ class DiscordWebSocket:
payload = {
'op': self.PRESENCE,
'd': {
'game': activity,
'activities': activity,
'afk': afk,
'since': since,
'status': status
@@ -707,13 +708,18 @@ class DiscordVoiceWebSocket:
CLIENT_CONNECT = 12
CLIENT_DISCONNECT = 13
def __init__(self, socket, loop):
def __init__(self, socket, loop, *, hook=None):
self.ws = socket
self.loop = loop
self._keep_alive = None
self._close_code = None
self.secret_key = None
if hook:
self._hook = hook
async def _hook(self, *args):
pass
async def send_as_json(self, data):
log.debug('Sending voice websocket frame: %s.', data)
await self.ws.send_str(utils.to_json(data))
@@ -746,12 +752,12 @@ class DiscordVoiceWebSocket:
await self.send_as_json(payload)
@classmethod
async def from_client(cls, client, *, resume=False):
async def from_client(cls, client, *, resume=False, hook=None):
"""Creates a voice websocket for the :class:`VoiceClient`."""
gateway = 'wss://' + client.endpoint + '/?v=4'
http = client._state.http
socket = await http.ws_connect(gateway, compress=15)
ws = cls(socket, loop=client.loop)
ws = cls(socket, loop=client.loop, hook=hook)
ws.gateway = gateway
ws._connection = client
ws._max_heartbeat_timeout = 60.0
@@ -818,6 +824,8 @@ class DiscordVoiceWebSocket:
interval = data['heartbeat_interval'] / 1000.0
self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=min(interval, 5.0))
self._keep_alive.start()
await self._hook(self, msg)
async def initial_connection(self, data):
state = self._connection

File diff suppressed because it is too large Load Diff

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 Optional, TYPE_CHECKING, overload, Type, Tuple
from .utils import _get_as_snowflake, get, parse_time
from .user import User
from .errors import InvalidArgument
@@ -30,9 +33,25 @@ from .enums import try_enum, ExpireBehaviour
__all__ = (
'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.
@@ -48,12 +67,13 @@ class IntegrationAccount:
__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: Optional[int] = _get_as_snowflake(data, 'id')
self.name: str = data.pop('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,76 @@ 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) -> None:
"""|coro|
Deletes the integration.
You must have the :attr:`~Permissions.manage_guild` permission to
do this.
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)
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 +164,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 +178,49 @@ 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_behavior',
'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: int = int(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 role(self) -> Optional[Role]:
"""Optional[:class:`Role`] The role which the integration uses for subscribers."""
return self.guild.get_role(self._role_id)
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'])
@overload
async def edit(
self,
*,
expire_behaviour: Optional[ExpireBehaviour] = ...,
expire_grace_period: Optional[int] = ...,
enable_emoticons: Optional[bool] = ...,
) -> None:
...
self.user = User(state=self._state, data=integ['user'])
self.account = IntegrationAccount(**integ['account'])
@overload
async def edit(self, **fields) -> None:
...
async def edit(self, **fields):
async def edit(self, **fields) -> None:
"""|coro|
Edits the integration.
@@ -161,9 +261,11 @@ class Integration:
'expire_grace_period': expire_grace_period,
}
enable_emoticons = fields.get('enable_emoticons')
if enable_emoticons is not None:
try:
enable_emoticons = fields['enable_emoticons']
except KeyError:
enable_emoticons = self.enable_emoticons
else:
payload['enable_emoticons'] = enable_emoticons
await self._state.http.edit_integration(self.guild.id, self.id, **payload)
@@ -173,7 +275,7 @@ class Integration:
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 +293,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,14 +25,37 @@ DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from discord.types.interactions import InteractionResponse
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Union
from . import utils
from .enums import try_enum, InteractionType
from .enums import try_enum, InteractionType, InteractionResponseType
from .user import User
from .member import Member
from .message import Message, Attachment
from .object import Object
from .webhook.async_ import async_context, Webhook
__all__ = (
'Interaction',
'InteractionResponse',
)
if TYPE_CHECKING:
from .types.interactions import (
Interaction as InteractionPayload,
)
from .guild import Guild
from .abc import GuildChannel
from .state import ConnectionState
from aiohttp import ClientSession
from .embeds import Embed
from .ui.view import View
MISSING: Any = utils.MISSING
class Interaction:
"""Represents a Discord interaction.
@@ -56,28 +79,36 @@ 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.
"""
__slots__ = (
__slots__: Tuple[str, ...] = (
'id',
'type',
'guild_id',
'channel_id',
'data',
'application_id',
'message',
'user',
'token',
'version',
'_state',
'_session',
'_cs_response',
'_cs_followup',
)
def __init__(self, *, data, state=None):
def __init__(self, *, data: InteractionPayload, state: ConnectionState):
self._state = state
self._session: ClientSession = state.http._HTTPClient__session
self._from_data(data)
def _from_data(self, data):
def _from_data(self, data: InteractionPayload):
self.id = int(data['id'])
self.type = try_enum(InteractionType, data['type'])
self.data = data.get('data')
@@ -87,13 +118,34 @@ class Interaction:
self.guild_id = utils._get_as_snowflake(data, 'guild_id')
self.application_id = utils._get_as_snowflake(data, 'application_id')
@property
def guild(self):
"""Optional[:class:`Guild`]: The guild the interaction was sent from."""
return self._state and self._state.get_guild(self.guild_id)
channel = self.channel or Object(id=self.channel_id)
try:
self.message = Message(state=self._state, channel=channel, data=data['message'])
except KeyError:
self.message = None
self.user: Optional[Union[User, Member]] = None
# TODO: there's a potential data loss here
if self.guild_id:
guild = self.guild or Object(id=self.guild_id)
try:
self.user = Member(state=self._state, guild=guild, data=data['member'])
except KeyError:
pass
else:
try:
self.user = User(state=self._state, data=data['user'])
except KeyError:
pass
@property
def channel(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)
@property
def channel(self) -> Optional[GuildChannel]:
"""Optional[:class:`abc.GuildChannel`]: The channel the interaction was sent from.
Note that due to a Discord limitation, DM channels are not resolved since there is
@@ -102,3 +154,275 @@ class Interaction:
guild = self.guild
return guild and guild.get_channel(self.channel_id)
@utils.cached_slot_property('_cs_response')
def response(self) -> InteractionResponse:
""":class:`InteractionResponse`: Returns an object responsible for handling responding to the interaction."""
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)
class InteractionResponse:
"""Represents a Discord interaction response.
This type can be accessed through :attr:`Interaction.response`.
.. versionadded:: 2.0
"""
__slots__: Tuple[str, ...] = (
'_responded',
'_parent',
)
def __init__(self, parent: Interaction):
self._parent: Interaction = parent
self._responded: bool = False
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.
"""
if self._responded:
return
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 = True
async def pong(self) -> None:
"""|coro|
Pongs the ping interaction.
This should rarely be used.
Raises
-------
HTTPException
Ponging the interaction failed.
"""
if self._responded:
return
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 = True
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.
"""
if self._responded:
return
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 = True
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``.
"""
if self._responded:
return
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
# TODO: embeds: List[Embed]?
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 = True

View File

@@ -22,11 +22,15 @@ 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',
@@ -34,6 +38,26 @@ __all__ = (
'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.
@@ -70,27 +94,28 @@ class PartialInviteChannel:
__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}>'
@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.
@@ -239,23 +232,25 @@ class Invite(Hashable):
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.
@@ -280,105 +275,185 @@ 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'
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')
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 = None if inviter_data is None else self._state.store_user(inviter_data)
self.channel = data.get('channel')
self.inviter: Optional[User] = None if inviter_data is None else self._state.store_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.store_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 __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):
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
async def delete(self, *, reason=None):
async def delete(self, *, reason: Optional[str] = None):
"""|coro|
Revokes the instant invite.

View File

@@ -26,10 +26,10 @@ 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
@@ -42,24 +42,46 @@ __all__ = (
)
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]]]
_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('__')
obj = elem
@@ -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()
@@ -91,7 +113,7 @@ class _AsyncIterator(AsyncIterator[T]):
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,6 +181,7 @@ class _FilteredAsyncIterator(_AsyncIterator[T]):
if ret:
return item
class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
def __init__(self, message, emoji, limit=100, after=None):
self.message = message
@@ -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,7 +212,9 @@ 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
@@ -203,6 +232,7 @@ class ReactionIterator(_AsyncIterator[Union['User', 'Member']]):
else:
await self.users.put(User(state=self.state, data=element))
class HistoryIterator(_AsyncIterator['Message']):
"""Iterator for receiving a channel's message history.
@@ -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))
@@ -272,7 +301,7 @@ class HistoryIterator(_AsyncIterator['Message']):
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
elif self.before:
@@ -281,15 +310,15 @@ class HistoryIterator(_AsyncIterator['Message']):
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._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._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()
@@ -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,14 +356,14 @@ 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
@@ -344,7 +373,7 @@ class HistoryIterator(_AsyncIterator['Message']):
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
@@ -355,11 +384,12 @@ class HistoryIterator(_AsyncIterator['Message']):
"""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']):
def __init__(self, guild, limit=None, before=None, after=None, oldest_first=None, user_id=None, action_type=None):
if isinstance(before, datetime.datetime):
@@ -367,7 +397,6 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
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,12 +413,10 @@ 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:
@@ -401,8 +428,9 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
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', [])
if len(data) and entries:
@@ -413,8 +441,9 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
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)
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:
@@ -422,7 +451,7 @@ class AuditLogIterator(_AsyncIterator['AuditLogEntry']):
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)
@@ -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._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,14 +584,14 @@ 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
@@ -570,13 +601,14 @@ class GuildIterator(_AsyncIterator['Guild']):
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']))
return data
class MemberIterator(_AsyncIterator['Member']):
def __init__(self, guild, limit=1000, after=None):
@@ -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,7 +650,7 @@ 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']))
@@ -627,4 +659,95 @@ class MemberIterator(_AsyncIterator['Member']):
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, data=data)

View File

@@ -22,16 +22,18 @@ 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 List, Literal, Optional, TYPE_CHECKING, Union, overload
import discord.abc
from . import utils
from .errors import ClientException
from .user import BaseUser, User
from .activity import create_activity
from .permissions import Permissions
@@ -44,6 +46,12 @@ __all__ = (
'Member',
)
if TYPE_CHECKING:
from .channel import VoiceChannel, StageChannel
from .abc import Snowflake
VocalGuildChannel = Union[VoiceChannel, StageChannel]
class VoiceState:
"""Represents a Discord user's voice state.
@@ -192,6 +200,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`]
@@ -218,7 +233,7 @@ class Member(discord.abc.Messageable, _BaseUser):
self._client_status = {
None: 'offline'
}
self.activities = tuple(map(create_activity, data.get('activities', [])))
self.activities = []
self.nick = data.get('nick', None)
self.pending = data.get('pending', False)
@@ -262,17 +277,6 @@ class Member(discord.abc.Messageable, _BaseUser):
member_data['user'] = data
return cls(data=member_data, guild=guild, state=state)
@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__
@@ -316,7 +320,7 @@ class Member(discord.abc.Messageable, _BaseUser):
self._update_roles(data)
def _presence_update(self, data, user):
self.activities = tuple(map(create_activity, data.get('activities', [])))
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()
@@ -329,12 +333,12 @@ class Member(discord.abc.Messageable, _BaseUser):
def _update_inner_user(self, 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))
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
@@ -444,6 +448,12 @@ class Member(discord.abc.Messageable, _BaseUser):
"""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`.
@@ -472,27 +482,6 @@ 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):
""":class:`Role`: Returns the member's highest role.
@@ -513,8 +502,7 @@ class Member(discord.abc.Messageable, _BaseUser):
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.
@@ -537,6 +525,19 @@ class Member(discord.abc.Messageable, _BaseUser):
"""Optional[:class:`VoiceState`]: Returns the member's current voice state."""
return self.guild._voice_state_for(self._user.id)
@overload
async def ban(
self,
*,
reason: Optional[str] = ...,
delete_message_days: Literal[1, 2, 3, 4, 5, 6, 7] = ...,
) -> None:
...
@overload
async def ban(self) -> None:
...
async def ban(self, **kwargs):
"""|coro|
@@ -544,20 +545,38 @@ class Member(discord.abc.Messageable, _BaseUser):
"""
await self.guild.ban(self, **kwargs)
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)
@overload
async def edit(
self,
*,
reason: Optional[str] = ...,
nick: Optional[str] = None,
mute: bool = ...,
deafen: bool = ...,
suppress: bool = ...,
roles: Optional[List[discord.abc.Snowflake]] = ...,
voice_channel: Optional[VocalGuildChannel] = ...,
) -> None:
...
@overload
async def edit(self) -> None:
...
async def edit(self, *, reason=None, **fields):
"""|coro|
@@ -705,7 +724,7 @@ class Member(discord.abc.Messageable, _BaseUser):
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 +747,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):
r"""|coro|
Gives the member a number of :class:`Role`\s.
@@ -767,7 +786,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.
@@ -811,3 +830,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[discord.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,10 +22,18 @@ 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 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'
@@ -36,7 +44,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.
@@ -70,14 +82,21 @@ class AllowedMentions:
__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,14 +104,14 @@ 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 = {}
@@ -113,9 +132,9 @@ class AllowedMentions:
data['replied_user'] = True
data['parse'] = parse
return data
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 +144,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

@@ -22,18 +22,24 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from typing import TypeVar
__all__ = (
'EqualityComparable',
'Hashable',
)
E = TypeVar('E', bound='EqualityComparable')
class EqualityComparable:
__slots__ = ()
def __eq__(self, other):
id: int
def __eq__(self: E, other: E) -> bool:
return isinstance(other, self.__class__) and other.id == self.id
def __ne__(self, other):
def __ne__(self: E, other: E) -> bool:
if isinstance(other, self.__class__):
return other.id != self.id
return True
@@ -41,5 +47,5 @@ class EqualityComparable:
class Hashable(EqualityComparable):
__slots__ = ()
def __hash__(self):
def __hash__(self) -> int:
return self.id >> 22

View File

@@ -22,9 +22,21 @@ 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
from typing import (
SupportsInt,
TYPE_CHECKING,
Union,
)
if TYPE_CHECKING:
import datetime
SupportsIntCast = Union[SupportsInt, str, bytes, bytearray]
__all__ = (
'Object',
)
@@ -63,7 +75,7 @@ class Object(Hashable):
The ID of the object.
"""
def __init__(self, id):
def __init__(self, id: SupportsIntCast):
try:
id = int(id)
except ValueError:
@@ -71,10 +83,10 @@ class Object(Hashable):
else:
self.id = id
def __repr__(self):
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,17 +22,37 @@ 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
import re
from .asset import Asset, AssetMixin
from .errors import InvalidArgument
from . import utils
__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:
@@ -72,35 +92,80 @@ class PartialEmoji(_EmojiTag):
__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: Dict[str, Any]) -> PE:
return cls(
animated=data.get('animated', False),
id=utils._get_as_snowflake(data, 'id'),
name=data.get('name'),
name=data.get('name', ''),
)
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
if 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:
@@ -108,9 +173,9 @@ class PartialEmoji(_EmojiTag):
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 +183,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}'
@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

@@ -140,7 +140,7 @@ class Permissions(BaseFlags):
"""A factory method that creates a :class:`Permissions` with all
permissions set to ``True``.
"""
return cls(0b111111111111111111111111111111111)
return cls(0b111111111111111111111111111111111111)
@classmethod
def all_channel(cls):
@@ -471,6 +471,39 @@ class Permissions(BaseFlags):
"""
return 1 << 32
@flag_value
def manage_events(self):
""":class:`bool`: Returns ``True`` if a user can manage guild events.
.. versionadded:: 2.0
"""
return 1 << 33
@flag_value
def manage_threads(self):
""":class:`bool`: Returns ``True`` if a user can manage threads.
.. versionadded:: 2.0
"""
return 1 << 34
@flag_value
def use_threads(self):
""":class:`bool`: Returns ``True`` if a user can create and participate in public threads.
.. versionadded:: 2.0
"""
return 1 << 35
@flag_value
def use_private_threads(self):
""":class:`bool`: Returns ``True`` if a user can create and participate in private threads.
.. versionadded:: 2.0
"""
return 1 << 36
def augment_from_permissions(cls):
cls.VALID_NAMES = set(Permissions.VALID_FLAGS)
aliases = set()

View File

@@ -73,7 +73,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).
@@ -144,7 +144,7 @@ class FFmpegAudio(AudioSource):
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

View File

@@ -29,6 +29,7 @@ __all__ = (
'RawReactionActionEvent',
'RawReactionClearEvent',
'RawReactionClearEmojiEvent',
'RawIntegrationDeleteEvent',
)
class _RawReprMixin:
@@ -222,3 +223,29 @@ class RawReactionClearEmojiEvent(_RawReprMixin):
self.guild_id = int(data['guild_id'])
except KeyError:
self.guild_id = 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):
self.integration_id = int(data['id'])
self.guild_id = int(data['guild_id'])
try:
self.application_id = int(data['application_id'])
except KeyError:
self.application_id = None

View File

@@ -93,7 +93,7 @@ class Reaction:
return str(self.emoji)
def __repr__(self):
return '<Reaction emoji={0.emoji!r} me={0.me} count={0.count}>'.format(self)
return f'<Reaction emoji={self.emoji!r} me={self.me} count={self.count}>'
async def remove(self, user):
"""|coro|
@@ -158,14 +158,14 @@ 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
------------
@@ -191,7 +191,7 @@ class Reaction:
"""
if self.custom_emoji:
emoji = '{0.name}:{0.id}'.format(self.emoji)
emoji = f'{self.emoji.name}:{self.emoji.id}'
else:
emoji = self.emoji

View File

@@ -22,17 +22,31 @@ 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, 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',
)
if TYPE_CHECKING:
import datetime
from .types.role import (
Role as RolePayload,
RoleTags as RoleTagPayload,
)
from .guild import Guild
from .member import Member
from .state import ConnectionState
class RoleTags:
"""Represents tags on a role.
@@ -52,32 +66,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`.
@@ -129,6 +153,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,22 +171,33 @@ 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 __repr__(self) -> str:
return f'<Role id={self.id} name={self.name!r}>'
def __lt__(self, other):
def __lt__(self: R, other: R) -> bool:
if not isinstance(other, Role) or not isinstance(self, Role):
return NotImplemented
@@ -174,87 +218,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'])
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}>'
@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 +316,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")
@@ -286,7 +339,25 @@ class Role(Hashable):
payload = [{"id": z[0], "position": z[1]} for z in zip(roles, change_range)]
await http.move_role_position(self.guild.id, payload, reason=reason)
async def edit(self, *, reason=None, **fields):
@overload
async def edit(
self,
*,
reason: Optional[str] = ...,
name: str = ...,
permissions: Permissions = ...,
colour: Union[Colour, int] = ...,
hoist: bool = ...,
mentionable: bool = ...,
position: int = ...,
) -> None:
...
@overload
async def edit(self) -> None:
...
async def edit(self, *, reason=None, **fields) -> None:
"""|coro|
Edits the role.
@@ -346,13 +417,13 @@ class Role(Hashable):
'permissions': str(fields.get('permissions', self.permissions).value),
'color': colour.value,
'hoist': fields.get('hoist', self.hoist),
'mentionable': fields.get('mentionable', self.mentionable)
'mentionable': fields.get('mentionable', self.mentionable),
}
data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload)
self._update(data)
async def delete(self, *, reason=None):
async def delete(self, *, reason: Optional[str] = None) -> None:
"""|coro|
Deletes the role.

View File

@@ -359,42 +359,6 @@ class AutoShardedClient(Client):
"""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() }
@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):
try:
coro = DiscordWebSocket.from_client(self, initial=initial, gateway=gateway, shard_id=shard_id)

168
discord/stage_instance.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 __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 stagea instances are equal.
.. describe:: x != y
Checks if two stage instances are not equal.
.. describe:: hash(x)
Returns the stage instance's hash.
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 the stage instance is discoverable.
"""
__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 = try_enum(StagePrivacyLevel, data['privacy_level'])
self.discoverable_disabled = data['discoverable_disabled']
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 guild that stage instance is running in."""
return self._state.get_channel(self.channel_id)
def is_public(self) -> bool:
return self.privacy_level is StagePrivacyLevel.public
async def edit(self, *, topic: str = MISSING, privacy_level: StagePrivacyLevel = MISSING) -> 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.
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)
async def delete(self) -> None:
"""|coro|
Deletes the stage instance.
You must have the :attr:`~Permissions.manage_channels` permission to
use this.
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)

View File

@@ -28,6 +28,7 @@ import copy
import datetime
import itertools
import logging
from typing import Optional
import weakref
import warnings
import inspect
@@ -43,15 +44,21 @@ from .mentions import AllowedMentions
from .partial_emoji import PartialEmoji
from .message import Message
from .channel import *
from .channel import _channel_factory
from .raw_models import *
from .member import Member
from .role import Role
from .enums import ChannelType, try_enum, Status
from . import utils
from .flags import Intents, MemberCacheFlags
from .flags import ApplicationFlags, Intents, MemberCacheFlags
from .object import Object
from .invite import Invite
from .integrations import _integration_factory
from .interactions import Interaction
from .ui.view import ViewStore
from .stage_instance import StageInstance
from .threads import Thread, ThreadMember
from discord import guild
class ChunkRequest:
def __init__(self, guild_id, loop, resolver, *, cache=True):
@@ -120,7 +127,6 @@ class ConnectionState:
if self.guild_ready_timeout < 0:
raise ValueError('guild_ready_timeout cannot be negative')
self.guild_subscriptions = options.get('guild_subscriptions', True)
allowed_mentions = options.get('allowed_mentions')
if allowed_mentions is not None and not isinstance(allowed_mentions, AllowedMentions):
@@ -153,15 +159,7 @@ class ConnectionState:
if not intents.guilds:
log.warning('Guilds intent seems to be disabled. This may cause state related issues.')
try:
chunk_guilds = options['fetch_offline_members']
except KeyError:
chunk_guilds = options.get('chunk_guilds_at_startup', intents.members)
else:
msg = 'fetch_offline_members is deprecated, use chunk_guilds_at_startup instead'
warnings.warn(msg, DeprecationWarning, stacklevel=4)
self._chunk_guilds = chunk_guilds
self._chunk_guilds = options.get('chunk_guilds_at_startup', intents.members)
# Ensure these two are set properly
if not intents.members and self._chunk_guilds:
@@ -196,6 +194,7 @@ class ConnectionState:
self._users = weakref.WeakValueDictionary()
self._emojis = {}
self._guilds = {}
self._view_store = ViewStore(self)
self._voice_clients = {}
# LRU of max size 128
@@ -287,6 +286,16 @@ class ConnectionState:
self._emojis[emoji_id] = emoji = Emoji(guild=guild, state=self, data=data)
return emoji
def store_view(self, view, message_id=None):
self._view_store.add_view(view, message_id)
def prevent_view_updates_for(self, message_id):
return self._view_store.remove_message_tracking(message_id)
@property
def persistent_views(self):
return self._view_store.persistent_views
@property
def guilds(self):
return list(self._guilds.values())
@@ -338,10 +347,10 @@ class ConnectionState:
if len(self._private_channels) > 128:
_, to_remove = self._private_channels.popitem(last=False)
if isinstance(to_remove, DMChannel):
if isinstance(to_remove, DMChannel) and to_remove.recipient:
self._private_channels_by_user.pop(to_remove.recipient.id, None)
if isinstance(channel, DMChannel):
if isinstance(channel, DMChannel) and channel.recipient:
self._private_channels_by_user[channel.recipient.id] = channel
def add_dm_channel(self, data):
@@ -371,10 +380,10 @@ class ConnectionState:
try:
guild = self._get_guild(int(data['guild_id']))
except KeyError:
channel = self.get_channel(channel_id)
channel = DMChannel._from_message(self, channel_id)
guild = None
else:
channel = guild and guild.get_channel(channel_id)
channel = guild and (guild.get_channel(channel_id) or guild.get_thread(channel_id))
return channel or Object(id=channel_id), guild
@@ -461,6 +470,7 @@ class ConnectionState:
pass
else:
self.application_id = utils._get_as_snowflake(application, 'id')
self.application_flags = ApplicationFlags._from_value(application['flags'])
for guild_data in data['guilds']:
self._add_guild_from_data(guild_data)
@@ -477,7 +487,7 @@ class ConnectionState:
self.dispatch('message', message)
if self._messages is not None:
self._messages.append(message)
if channel and channel.__class__ is TextChannel:
if channel and channel.__class__ in (TextChannel, Thread):
channel.last_message_id = message.id
def parse_message_delete(self, data):
@@ -517,6 +527,9 @@ class ConnectionState:
else:
self.dispatch('raw_message_edit', raw)
if 'components' in data and self._view_store.is_message_tracked(raw.message_id):
self._view_store.update_from_message(raw.message_id, data['components'])
def parse_message_reaction_add(self, data):
emoji = data['emoji']
emoji_id = utils._get_as_snowflake(emoji, 'id')
@@ -589,6 +602,11 @@ class ConnectionState:
def parse_interaction_create(self, data):
interaction = Interaction(data=data, state=self)
if data['type'] == 3: # interaction component
custom_id = interaction.data['custom_id'] # type: ignore
component_type = interaction.data['component_type'] # type: ignore
self._view_store.dispatch(component_type, custom_id, interaction)
self.dispatch('interaction', interaction)
def parse_presence_update(self, data):
@@ -601,24 +619,14 @@ class ConnectionState:
user = data['user']
member_id = int(user['id'])
member = guild.get_member(member_id)
flags = self.member_cache_flags
if member is None:
if 'username' not in user:
# sometimes we receive 'incomplete' member data post-removal.
# skip these useless cases.
return
log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding', member_id)
return
member, old_member = Member._from_presence_update(guild=guild, data=data, state=self)
if flags.online or (flags._online_only and member.raw_status != 'offline'):
guild._add_member(member)
else:
old_member = Member._copy(member)
user_update = member._presence_update(data=data, user=user)
if user_update:
self.dispatch('user_update', user_update[0], user_update[1])
if member.id != self.self_id and flags._online_only and member.raw_status == 'offline':
guild._remove_member(member)
old_member = Member._copy(member)
user_update = member._presence_update(data=data, user=user)
if user_update:
self.dispatch('user_update', user_update[0], user_update[1])
self.dispatch('member_update', old_member, member)
@@ -641,13 +649,6 @@ class ConnectionState:
if channel is not None:
guild._remove_channel(channel)
self.dispatch('guild_channel_delete', channel)
else:
# the reason we're doing this is so it's also removed from the
# private channel by user cache as well
channel = self._get_private_channel(channel_id)
if channel is not None:
self._remove_private_channel(channel)
self.dispatch('private_channel_delete', channel)
def parse_channel_update(self, data):
channel_type = try_enum(ChannelType, data.get('type'))
@@ -678,22 +679,15 @@ class ConnectionState:
log.debug('CHANNEL_CREATE referencing an unknown channel type %s. Discarding.', data['type'])
return
if ch_type in (ChannelType.group, ChannelType.private):
channel_id = int(data['id'])
if self._get_private_channel(channel_id) is None:
channel = factory(me=self.user, data=data, state=self)
self._add_private_channel(channel)
self.dispatch('private_channel_create', channel)
guild_id = utils._get_as_snowflake(data, 'guild_id')
guild = self._get_guild(guild_id)
if guild is not None:
channel = factory(guild=guild, state=self, data=data)
guild._add_channel(channel)
self.dispatch('guild_channel_create', channel)
else:
guild_id = utils._get_as_snowflake(data, 'guild_id')
guild = self._get_guild(guild_id)
if guild is not None:
channel = factory(guild=guild, state=self, data=data)
guild._add_channel(channel)
self.dispatch('guild_channel_create', channel)
else:
log.debug('CHANNEL_CREATE referencing an unknown guild ID: %s. Discarding.', guild_id)
return
log.debug('CHANNEL_CREATE referencing an unknown guild ID: %s. Discarding.', guild_id)
return
def parse_channel_pins_update(self, data):
channel_id = int(data['channel_id'])
@@ -714,6 +708,132 @@ class ConnectionState:
else:
self.dispatch('guild_channel_pins_update', channel, last_pin)
def parse_thread_create(self, data):
guild_id = int(data['guild_id'])
guild: Optional[Guild] = self._get_guild(guild_id)
if guild is None:
log.debug('THREAD_CREATE referencing an unknown guild ID: %s. Discarding', guild_id)
return
thread = Thread(guild=guild, data=data)
has_thread = guild.get_thread(thread.id)
guild._add_thread(thread)
if not has_thread:
self.dispatch('thread_join', thread)
def parse_thread_update(self, data):
guild_id = int(data['guild_id'])
guild = self._get_guild(guild_id)
if guild is None:
log.debug('THREAD_UPDATE referencing an unknown guild ID: %s. Discarding', guild_id)
return
thread_id = int(data['id'])
thread = guild.get_thread(thread_id)
if thread is not None:
old = copy.copy(thread)
thread._update(data)
self.dispatch('thread_update', old, thread)
def parse_thread_delete(self, data):
guild_id = int(data['guild_id'])
guild = self._get_guild(guild_id)
if guild is None:
log.debug('THREAD_DELETE referencing an unknown guild ID: %s. Discarding', guild_id)
return
thread_id = int(data['id'])
thread = guild.get_thread(thread_id)
if thread is not None:
guild._remove_thread(thread)
self.dispatch('thread_delete', thread)
def parse_thread_list_sync(self, data):
guild_id = int(data['guild_id'])
guild: Optional[Guild] = self._get_guild(guild_id)
if guild is None:
log.debug('THREAD_LIST_SYNC referencing an unknown guild ID: %s. Discarding', guild_id)
return
try:
channel_ids = set(data['channel_ids'])
except KeyError:
# If not provided, then the entire guild is being synced
# So all previous thread data should be overwritten
previous_threads = guild._threads.copy()
guild._clear_threads()
else:
previous_threads = guild._filter_threads(channel_ids)
threads = {
d['id']: guild._store_thread(d)
for d in data.get('threads', [])
}
for member in data.get('members', []):
try:
# note: member['id'] is the thread_id
thread = threads[member['id']]
except KeyError:
continue
else:
thread._add_member(ThreadMember(thread, member))
for thread in threads.values():
old = previous_threads.pop(thread.id, None)
if old is None:
self.dispatch('thread_join', thread)
for thread in previous_threads.values():
self.dispatch('thread_remove', thread)
def parse_thread_member_update(self, data):
guild_id = int(data['guild_id'])
guild: Optional[Guild] = self._get_guild(guild_id)
if guild is None:
log.debug('THREAD_MEMBER_UPDATE referencing an unknown guild ID: %s. Discarding', guild_id)
return
thread_id = int(data['id'])
thread: Optional[Thread] = guild.get_thread(thread_id)
if thread is None:
log.debug('THREAD_MEMBER_UPDATE referencing an unknown thread ID: %s. Discarding', thread_id)
return
member = ThreadMember(thread, data)
thread.me = member
def parse_thread_members_update(self, data):
guild_id = int(data['guild_id'])
guild: Optional[Guild] = self._get_guild(guild_id)
if guild is None:
log.debug('THREAD_MEMBERS_UPDATE referencing an unknown guild ID: %s. Discarding', guild_id)
return
thread_id = int(data['id'])
thread: Optional[Thread] = guild.get_thread(thread_id)
if thread is None:
log.debug('THREAD_MEMBERS_UPDATE referencing an unknown thread ID: %s. Discarding', thread_id)
return
added_members = [ThreadMember(thread, d) for d in data.get('added_members', [])]
removed_member_ids = data.get('removed_member_ids', [])
self_id = self.self_id
for member in added_members:
if member.id != self_id:
thread._add_member(member)
self.dispatch('thread_member_join', member)
else:
thread.me = member
self.dispatch('thread_join', thread)
for member_id in removed_member_ids:
if member_id != self_id:
member = thread._pop_member(member_id)
self.dispatch('thread_member_leave', member)
else:
self.dispatch('thread_remove', thread)
def parse_guild_member_add(self, data):
guild = self._get_guild(int(data['guild_id']))
if guild is None:
@@ -801,6 +921,9 @@ class ConnectionState:
return self._add_guild_from_data(data)
def is_guild_evicted(self, guild) -> bool:
return guild.id not in self._guilds
async def chunk_guild(self, guild, *, wait=True, cache=None):
cache = cache or self.member_cache_flags.joined
request = self._chunk_requests.get(guild.id)
@@ -965,6 +1088,35 @@ class ConnectionState:
else:
log.debug('GUILD_INTEGRATIONS_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id'])
def parse_integration_create(self, data):
guild_id = int(data.pop('guild_id'))
guild = self._get_guild(guild_id)
if guild is not None:
cls, _ = _integration_factory(data['type'])
integration = cls(data=data, guild=guild)
self.dispatch('integration_create', integration)
else:
log.debug('INTEGRATION_CREATE referencing an unknown guild ID: %s. Discarding.', guild_id)
def parse_integration_update(self, data):
guild_id = int(data.pop('guild_id'))
guild = self._get_guild(guild_id)
if guild is not None:
cls, _ = _integration_factory(data['type'])
integration = cls(data=data, guild=guild)
self.dispatch('integration_update', integration)
else:
log.debug('INTEGRATION_UPDATE referencing an unknown guild ID: %s. Discarding.', guild_id)
def parse_integration_delete(self, data):
guild_id = int(data['guild_id'])
guild = self._get_guild(guild_id)
if guild is not None:
raw = RawIntegrationDeleteEvent(data)
self.dispatch('raw_integration_delete', raw)
else:
log.debug('INTEGRATION_DELETE referencing an unknown guild ID: %s. Discarding.', guild_id)
def parse_webhooks_update(self, data):
channel = self.get_channel(int(data['channel_id']))
if channel is not None:
@@ -972,6 +1124,40 @@ class ConnectionState:
else:
log.debug('WEBHOOKS_UPDATE referencing an unknown channel ID: %s. Discarding.', data['channel_id'])
def parse_stage_instance_create(self, data):
guild = self._get_guild(int(data['guild_id']))
if guild is not None:
stage_instance = StageInstance(guild=guild, state=self, data=data)
guild._stage_instances[stage_instance.id] = stage_instance
self.dispatch('stage_instance_create', stage_instance)
else:
log.debug('STAGE_INSTANCE_CREATE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
def parse_stage_instance_update(self, data):
guild = self._get_guild(int(data['guild_id']))
if guild is not None:
stage_instance = guild._stage_instances.get(int(data['id']))
if stage_instance is not None:
old_stage_instance = copy.copy(stage_instance)
stage_instance._update(data)
self.dispatch('stage_instance_update', old_stage_instance, stage_instance)
else:
log.debug('STAGE_INSTANCE_UPDATE referencing unknown stage instance ID: %s. Discarding.', data['id'])
else:
log.debug('STAGE_INSTANCE_UPDATE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
def parse_stage_instance_delete(self, data):
guild = self._get_guild(int(data['guild_id']))
if guild is not None:
try:
stage_instance = guild._stage_instances.pop(int(data['id']))
except KeyError:
pass
else:
self.dispatch('stage_instance_delete', stage_instance)
else:
log.debug('STAGE_INSTANCE_DELETE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
def parse_voice_state_update(self, data):
guild = self._get_guild(utils._get_as_snowflake(data, 'guild_id'))
channel_id = utils._get_as_snowflake(data, 'channel_id')
@@ -1064,7 +1250,7 @@ class ConnectionState:
return pm
for guild in self.guilds:
channel = guild.get_channel(id)
channel = guild.get_channel(id) or guild.get_thread(id)
if channel is not None:
return channel
@@ -1086,7 +1272,7 @@ class AutoShardedConnectionState(ConnectionState):
new_guild = self._get_guild(msg.guild.id)
if new_guild is not None and new_guild is not msg.guild:
channel_id = msg.channel.id
channel = new_guild.get_channel(channel_id) or Object(id=channel_id)
channel = new_guild.get_channel(channel_id) or new_guild.get_thread(channel_id) or Object(id=channel_id)
msg._rebind_channel_reference(channel)
async def chunker(self, guild_id, query='', limit=0, presences=False, *, shard_id=None, nonce=None):
@@ -1174,6 +1360,7 @@ class AutoShardedConnectionState(ConnectionState):
pass
else:
self.application_id = utils._get_as_snowflake(application, 'id')
self.application_flags = ApplicationFlags._from_value(application['flags'])
for guild_data in data['guilds']:
self._add_guild_from_data(guild_data)

View File

@@ -22,6 +22,9 @@ 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 TYPE_CHECKING, List, Optional
from .mixins import Hashable
from .asset import Asset
from .utils import snowflake_time
@@ -31,8 +34,14 @@ __all__ = (
'Sticker',
)
if TYPE_CHECKING:
import datetime
from .state import ConnectionState
from .types.message import Sticker as StickerPayload
class Sticker(Hashable):
"""Represents a sticker
"""Represents a sticker.
.. versionadded:: 1.6
@@ -40,102 +49,72 @@ 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
The id of the sticker's pack.
format: :class:`StickerType`
The format for the sticker's image
image: :class:`str`
The sticker's image
The format for 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
A list of tags for the sticker.
"""
__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', 'pack_id', 'format', '_image', 'tags')
def __init__(self, *, state: ConnectionState, data: StickerPayload):
self._state: ConnectionState = state
self.id: int = int(data['id'])
self.name: str = data['name']
self.description: str = data['description']
self.pack_id: int = int(data['pack_id'])
self.format: StickerType = try_enum(StickerType, data['format_type'])
self._image: str = data['asset']
try:
self.tags = [tag.strip() for tag in data['tags'].split(',')]
self.tags: List[str] = [tag.strip() for tag in data['tags'].split(',')]
except KeyError:
self.tags = []
self.preview_image = data.get('preview_asset')
def __repr__(self) -> str:
return f'<{self.__class__.__name__} id={self.id} name={self.name!r}>'
def __repr__(self):
return '<{0.__class__.__name__} id={0.id} name={0.name!r}>'.format(self)
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):
def image(self) -> Optional[Asset]:
"""Returns an :class:`Asset` for the sticker's image.
.. note::
This will return ``None`` if the format is ``StickerType.lottie``
This will return ``None`` if the format is ``StickerType.lottie``.
Returns
-------
Optional[:class:`Asset`]
The resulting CDN asset.
"""
return self.image_url_as()
def image_url_as(self, *, size=1024):
"""Optionally returns an :class:`Asset` for the sticker's image.
The size must be a power of 2 between 16 and 4096.
.. note::
This will return ``None`` if the format is ``StickerType.lottie``.
Parameters
-----------
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Invalid ``size``.
Returns
-------
Optional[:class:`Asset`]
The resulting CDN asset or ``None``.
"""
if self.format is StickerType.lottie:
return None
return Asset._from_sticker_url(self._state, self, size=size)
return Asset._from_sticker(self._state, self.id, self._image)

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',
)
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']
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):
return '<{0.__class__.__name__} id={0.id} name={0.name!r} ' \
'discriminator={0.discriminator!r} membership_state={0.membership_state!r}>'.format(self)
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,6 +22,9 @@ 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, Optional, TYPE_CHECKING, overload
from .utils import parse_time, _get_as_snowflake, _bytes_to_base64_data
from .enums import VoiceRegion
from .guild import Guild
@@ -30,12 +33,17 @@ __all__ = (
'Template',
)
if TYPE_CHECKING:
from .types.template import Template as TemplatePayload
class _FriendlyHttpAttributeErrorHelper:
__slots__ = ()
def __getattr__(self, attr):
raise AttributeError('PartialTemplateState does not support http methods.')
class _PartialTemplateState:
def __init__(self, *, state):
self.__state = state
@@ -72,6 +80,7 @@ class _PartialTemplateState:
def __getattr__(self, attr):
raise AttributeError(f'PartialTemplateState does not support {attr!r}.')
class Template:
"""Represents a Discord template.
@@ -96,16 +105,33 @@ 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, data: TemplatePayload):
self._state = state
self._store(data)
def _store(self, data):
def _store(self, data: TemplatePayload):
self.code = data['code']
self.uses = data['usage_count']
self.name = data['name']
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)
@@ -117,19 +143,22 @@ class Template:
guild = self._state._get_guild(id)
if guild is None:
if guild is None and id:
source_serialised = data['serialized_source_guild']
source_serialised['id'] = id
state = _PartialTemplateState(state=self._state)
guild = Guild(data=source_serialised, state=state)
self.source_guild = guild
self.is_dirty = 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):
"""|coro|
Creates a :class:`.Guild` using the template.
@@ -169,7 +198,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) -> None:
"""|coro|
Sync the template to the guild's current state.
@@ -192,7 +221,20 @@ class Template:
data = await self._state.http.sync_template(self.source_guild.id, self.code)
self._store(data)
async def edit(self, **kwargs):
@overload
async def edit(
self,
*,
name: Optional[str] = ...,
description: Optional[str] = ...,
) -> None:
...
@overload
async def edit(self) -> None:
...
async def edit(self, **kwargs) -> None:
"""|coro|
Edit the template metadata.
@@ -221,7 +263,7 @@ class Template:
data = await self._state.http.edit_template(self.source_guild.id, self.code, kwargs)
self._store(data)
async def delete(self):
async def delete(self) -> None:
"""|coro|
Delete the template.
@@ -241,3 +283,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}'

686
discord/threads.py Normal file
View File

@@ -0,0 +1,686 @@
"""
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 .guild import Guild
from .channel import TextChannel
from .member import Member
from .message import Message
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:: 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.
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',
'archiver_id',
'auto_archive_duration',
'archive_timestamp',
)
def __init__(self, *, guild: Guild, data: ThreadPayload):
self._state: ConnectionState = guild._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._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)
def _update(self, data):
try:
self.name = data['name']
except KeyError:
pass
try:
self._unroll_metadata(data['thread_metadata'])
except KeyError:
pass
@property
def parent(self) -> Optional[TextChannel]:
"""Optional[:class:`TextChannel`]: The parent channel this thread belongs to."""
return self.guild.get_channel(self.parent_id)
@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 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_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 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 = [m.id for m in messages]
await self._state.http.delete_messages(self.id, message_ids)
async def purge(
self,
*,
limit: 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,
slowmode_delay: int = MISSING,
auto_archive_duration: ThreadArchiveDuration = MISSING,
):
"""|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.
auto_archive_duration: :class:`int`
The new duration to auto archive threads 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.
"""
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 slowmode_delay is not MISSING:
payload['rate_limit_per_user'] = slowmode_delay
await self._state.http.edit_channel(self.id, **payload)
async def join(self):
"""|coro|
Joins this thread.
You must have :attr:`~Permissions.send_messages` and :attr:`~Permissions.use_threads`
to join 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 join the thread.
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 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 _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:: 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

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: bool
mobile: bool
web: bool
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]

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

@@ -0,0 +1,66 @@
"""
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
class PartialAppInfo(_PartialAppInfoOptional, BaseAppInfo):
pass

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

@@ -0,0 +1,240 @@
"""
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
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,
]
class _AuditLogChange_Str(TypedDict):
key: Literal[
'name', 'description', 'preferred_locale', 'vanity_url_code', 'topic', 'code', 'allow', 'deny', 'permissions'
]
new_value: str
old_value: str
class _AuditLogChange_AssetHash(TypedDict):
key: Literal['icon_hash', 'splash_hash', 'discovery_splash_hash', 'banner_hash', 'avatar_hash']
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',
]
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',
]
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',
]
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]

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
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,103 @@ 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
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]
bitrate: int
user_limit: int
video_quality_mode: VideoQualityMode
class VoiceChannel(_BaseGuildChannel, _VoiceChannelOptional):
type: Literal[2]
class CategoryChannel(_BaseGuildChannel):
type: Literal[4]
class StoreChannel(_BaseGuildChannel):
type: Literal[6]
class _StageChannelOptional(TypedDict, total=False):
rtc_region: Optional[str]
bitrate: int
user_limit: int
topic: str
class StageChannel(_BaseGuildChannel, _StageChannelOptional):
type: Literal[13]
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[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,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, 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
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]

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

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

@@ -0,0 +1,159 @@
"""
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[
'INVITE_SPLASH',
'VIP_REGIONS',
'VANITY_URL',
'VERIFIED',
'PARTNERED',
'COMMUNITY',
'COMMERCE',
'NEWS',
'DISCOVERABLE',
'FEATURABLE',
'ANIMATED_ICON',
'BANNER',
'WELCOME_SCREEN_ENABLED',
'MEMBER_VERIFICATION_GATE_ENABLED',
'PREVIEW_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: Snowflake
enable_emoticons: bool
subscriber_count: int
revoked: bool
class BotIntegration(BaseIntegration):
application: IntegrationApplication
Integration = Union[BaseIntegration, StreamIntegration, BotIntegration]

View File

@@ -0,0 +1,189 @@
"""
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
class _ApplicationCommandOptional(TypedDict, total=False):
options: List[ApplicationCommandOption]
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]
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 _ApplicationCommandInteractionDataOptionOptional(TypedDict, total=False):
value: ApplicationCommandOptionType
options: List[ApplicationCommandInteractionDataOption]
class ApplicationCommandInteractionDataOption(
_ApplicationCommandInteractionDataOptionOptional
):
name: str
type: ApplicationCommandOptionType
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
class ApplicationCommandInteractionData(_ApplicationCommandInteractionDataOptional):
id: Snowflake
name: str
class _ComponentInteractionDataOptional(TypedDict, total=False):
values: List[str]
class ComponentInteractionData(_ComponentInteractionDataOptional):
custom_id: str
component_type: ComponentType
class _InteractionOptional(TypedDict, total=False):
data: Union[ApplicationCommandInteractionData, ComponentInteractionData]
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 EditApplicationCommand(TypedDict):
name: str
description: str
options: Optional[List[ApplicationCommandOption]]
default_permission: bool

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]

46
discord/types/member.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 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):
user: User
nick: str
premium_since: str
pending: bool
permissions: str

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

@@ -0,0 +1,153 @@
"""
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
from .user import User
from .emoji import PartialEmoji
from .embed import Embed
from .channel import ChannelType
from .components import Component
from .interactions import MessageInteraction
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
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 _StickerOptional(TypedDict, total=False):
tags: str
StickerFormatType = Literal[1, 2, 3]
class Sticker(_StickerOptional):
id: Snowflake
pack_id: Snowflake
name: str
description: str
asset: str
format_type: StickerFormatType
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
stickers: List[Sticker]
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[User]
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

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]

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

@@ -0,0 +1,43 @@
"""
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]

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

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

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

@@ -0,0 +1,61 @@
"""
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
from .member import Member
class _PartialVoiceStateOptional(TypedDict, total=False):
member: Member
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

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 *

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

@@ -0,0 +1,287 @@
"""
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, 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.
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,
label: Optional[str] = None,
disabled: bool = False,
custom_id: Optional[str] = None,
url: Optional[str] = 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=url,
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 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:
nonlocal custom_id
if not inspect.iscoroutinefunction(func):
raise TypeError('button function must be a coroutine function')
custom_id = custom_id or os.urandom(32).hex()
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`
"""
__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:
""":class:`int`: The width of the item."""
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

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

@@ -0,0 +1,333 @@
"""
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.
.. 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 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:`discord.SelectOption`]
A list of options that can be selected in this menu.
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',
)
def __init__(
self,
*,
custom_id: str = MISSING,
placeholder: Optional[str] = None,
min_values: int = 1,
max_values: int = 1,
options: List[SelectOption] = MISSING,
row: Optional[int] = None,
) -> None:
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,
)
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 25 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 50 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 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,
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,
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.
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 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:`discord.SelectOption`]
A list of options that can be selected in this menu.
"""
def decorator(func: ItemCallbackType) -> ItemCallbackType:
if not inspect.iscoroutinefunction(func):
raise TypeError('button 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,
}
return func
return decorator

463
discord/ui/view.py Normal file
View File

@@ -0,0 +1,463 @@
"""
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, ClassVar, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple
from functools import partial
from itertools import groupby
import traceback
import asyncio
import sys
import time
import os
from .item import Item, ItemCallbackType
from ..enums import ComponentType
from ..components import (
Component,
ActionRow as ActionRowComponent,
_component_factory,
Button as ButtonComponent,
)
__all__ = (
'View',
)
if TYPE_CHECKING:
from ..interactions import Interaction
from ..types.components import Component as ComponentPayload
from ..state import ConnectionState
def _walk_all_components(components: List[Component]) -> Iterator[Component]:
for item in components:
if isinstance(item, ActionRowComponent):
yield from item.children
else:
yield item
def _component_to_item(component: Component) -> Item:
if isinstance(component, ButtonComponent):
from .button import Button
return Button.from_component(component)
return Item.from_component(component)
class _ViewWeights:
__slots__ = (
'weights',
)
def __init__(self, children: List[Item]):
self.weights: List[int] = [0, 0, 0, 0, 0]
key = lambda i: sys.maxsize if i.row is None else i.row
children = sorted(children, key=key)
for row, group in groupby(children, key=key):
for item in group:
self.add_item(item)
def find_open_space(self, item: Item) -> int:
for index, weight in enumerate(self.weights):
if weight + item.width <= 5:
return index
raise ValueError('could not find open space for item')
def add_item(self, item: Item) -> None:
if item.row is not None:
total = self.weights[item.row] + item.width
if total > 5:
raise ValueError(f'item would not fit at row {item.row} ({total} > 5 width)')
self.weights[item.row] = total
item._rendered_row = item.row
else:
index = self.find_open_space(item)
self.weights[index] += item.width
item._rendered_row = index
def remove_item(self, item: Item) -> None:
if item._rendered_row is not None:
self.weights[item._rendered_row] -= item.width
item._rendered_row = None
def clear(self) -> None:
self.weights = [0, 0, 0, 0, 0]
class View:
"""Represents a UI view.
This object must be inherited to create a UI within Discord.
Parameters
-----------
timeout: Optional[:class:`float`]
Timeout from last interaction with the UI before no longer accepting input.
If ``None`` then there is no timeout.
Attributes
------------
timeout: Optional[:class:`float`]
Timeout from last interaction with the UI before no longer accepting input.
If ``None`` then there is no timeout.
children: List[:class:`Item`]
The list of children attached to this view.
"""
__discord_ui_view__: ClassVar[bool] = True
__view_children_items__: ClassVar[List[ItemCallbackType]] = []
def __init_subclass__(cls) -> None:
children: List[ItemCallbackType] = []
for base in reversed(cls.__mro__):
for member in base.__dict__.values():
if hasattr(member, '__discord_ui_model_type__'):
children.append(member)
if len(children) > 25:
raise TypeError('View cannot have more than 25 children')
cls.__view_children_items__ = children
def __init__(self, timeout: Optional[float] = 180.0):
self.timeout = timeout
self.children: List[Item] = []
for func in self.__view_children_items__:
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
item.callback = partial(func, self, item)
item._view = self
setattr(self, func.__name__, item)
self.children.append(item)
self.__weights = _ViewWeights(self.children)
loop = asyncio.get_running_loop()
self.id = os.urandom(16).hex()
self._cancel_callback: Optional[Callable[[View], None]] = None
self._timeout_handler: Optional[asyncio.TimerHandle] = None
self._stopped = loop.create_future()
def __repr__(self) -> str:
return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self.children)}>'
def to_components(self) -> List[Dict[str, Any]]:
def key(item: Item) -> int:
return item._rendered_row or 0
children = sorted(self.children, key=key)
components: List[Dict[str, Any]] = []
for _, group in groupby(children, key=key):
children = [item.to_component_dict() for item in group]
if not children:
continue
components.append(
{
'type': 1,
'components': children,
}
)
return components
@property
def _expires_at(self) -> Optional[float]:
if self.timeout:
return time.monotonic() + self.timeout
return None
def add_item(self, item: Item) -> None:
"""Adds an item to the view.
Parameters
-----------
item: :class:`Item`
The item to add to the view.
Raises
--------
TypeError
An :class:`Item` was not passed.
ValueError
Maximum number of children has been exceeded (25)
or the row the item is trying to be added to is full.
"""
if len(self.children) > 25:
raise ValueError('maximum number of children exceeded')
if not isinstance(item, Item):
raise TypeError(f'expected Item not {item.__class__!r}')
self.__weights.add_item(item)
item._view = self
self.children.append(item)
def remove_item(self, item: Item) -> None:
"""Removes an item from the view.
Parameters
-----------
item: :class:`Item`
The item to remove from the view.
"""
try:
self.children.remove(item)
except ValueError:
pass
else:
self.__weights.remove_item(item)
def clear_items(self) -> None:
"""Removes all items from the view."""
self.children.clear()
self.__weights.clear()
async def interaction_check(self, interaction: Interaction) -> bool:
"""|coro|
A callback that is called when an interaction happens within the view
that checks whether the view should process item callbacks for the interaction.
This is useful to override if, for example, you want to ensure that the
interaction author is a given user.
The default implementation of this returns ``True``.
.. note::
If an exception occurs within the body then the interaction
check then :meth:`on_error` is called and it is considered
a failure.
Parameters
-----------
interaction: :class:`~discord.Interaction`
The interaction that occurred.
Returns
---------
:class:`bool`
Whether the view children's callbacks should be called.
"""
return True
async def on_timeout(self) -> None:
"""|coro|
A callback that is called when a view's timeout elapses without being explicitly stopped.
"""
pass
async def on_error(self, error: Exception, item: Item, interaction: Interaction) -> None:
"""|coro|
A callback that is called when an item's callback or :meth:`interaction_check`
fails with an error.
The default implementation prints the traceback to stderr.
Parameters
-----------
error: :class:`Exception`
The exception that was raised.
item: :class:`Item`
The item that failed the dispatch.
interaction: :class:`~discord.Interaction`
The interaction that led to the failure.
"""
print(f'Ignoring exception in view {self} for item {item}:', file=sys.stderr)
traceback.print_exception(error.__class__, error, error.__traceback__, file=sys.stderr)
async def _scheduled_task(self, state: Any, item: Item, interaction: Interaction):
try:
allow = await self.interaction_check(interaction)
if not allow:
return
await item.callback(interaction)
if not interaction.response._responded:
await interaction.response.defer()
except Exception as e:
return await self.on_error(e, item, interaction)
def _start_listening(self, store: ViewStore) -> None:
self._cancel_callback = partial(store.remove_view)
if self.timeout:
loop = asyncio.get_running_loop()
self._timeout_handler = loop.call_later(self.timeout, self.dispatch_timeout)
def dispatch_timeout(self):
if self._stopped.done():
return
self._stopped.set_result(True)
asyncio.create_task(self.on_timeout(), name=f'discord-ui-view-timeout-{self.id}')
def dispatch(self, state: Any, item: Item, interaction: Interaction):
asyncio.create_task(self._scheduled_task(state, item, interaction), name=f'discord-ui-view-dispatch-{self.id}')
def refresh(self, components: List[Component]):
# This is pretty hacky at the moment
# fmt: off
old_state: Dict[Tuple[int, str], Item] = {
(item.type.value, item.custom_id): item # type: ignore
for item in self.children
if item.is_dispatchable()
}
# fmt: on
children: List[Item] = []
for component in _walk_all_components(components):
try:
older = old_state[(component.type.value, component.custom_id)] # type: ignore
except (KeyError, AttributeError):
children.append(_component_to_item(component))
else:
older.refresh_component(component)
children.append(older)
self.children = children
def stop(self) -> None:
"""Stops listening to interaction events from this view.
This operation cannot be undone.
"""
if not self._stopped.done():
self._stopped.set_result(False)
if self._timeout_handler:
self._timeout_handler.cancel()
if self._cancel_callback:
self._cancel_callback(self)
self._cancel_callback = None
def is_finished(self) -> bool:
""":class:`bool`: Whether the view has finished interacting."""
return self._stopped.done()
def is_dispatching(self) -> bool:
""":class:`bool`: Whether the view has been added for dispatching purposes."""
return self._cancel_callback is not None
def is_persistent(self) -> bool:
""":class:`bool`: Whether the view is set up as persistent.
A persistent view has all their components with a set ``custom_id`` and
a :attr:`timeout` set to ``None``.
"""
return self.timeout is None and all(item.is_persistent() for item in self.children)
async def wait(self) -> bool:
"""Waits until the view has finished interacting.
A view is considered finished when :meth:`stop` is called
or it times out.
Returns
--------
:class:`bool`
If ``True``, then the view timed out. If ``False`` then
the view finished normally.
"""
return await self._stopped
class ViewStore:
def __init__(self, state: ConnectionState):
# (component_type, custom_id): (View, Item, Expiry)
self._views: Dict[Tuple[int, str], Tuple[View, Item, Optional[float]]] = {}
# message_id: View
self._synced_message_views: Dict[int, View] = {}
self._state: ConnectionState = state
@property
def persistent_views(self) -> Sequence[View]:
views = {
view.id: view
for (_, (view, _, _)) in self._views.items()
if view.is_persistent()
}
return list(views.values())
def __verify_integrity(self):
to_remove: List[Tuple[int, str]] = []
now = time.monotonic()
for (k, (_, _, expiry)) in self._views.items():
if expiry is not None and now >= expiry:
to_remove.append(k)
for k in to_remove:
del self._views[k]
def add_view(self, view: View, message_id: Optional[int] = None):
self.__verify_integrity()
expiry = view._expires_at
view._start_listening(self)
for item in view.children:
if item.is_dispatchable():
self._views[(item.type.value, item.custom_id)] = (view, item, expiry) # type: ignore
if message_id is not None:
self._synced_message_views[message_id] = view
def remove_view(self, view: View):
for item in view.children:
if item.is_dispatchable():
self._views.pop((item.type.value, item.custom_id), None) # type: ignore
for key, value in self._synced_message_views.items():
if value.id == view.id:
del self._synced_message_views[key]
break
def dispatch(self, component_type: int, custom_id: str, interaction: Interaction):
self.__verify_integrity()
key = (component_type, custom_id)
value = self._views.get(key)
if value is None:
return
view, item, _ = value
self._views[key] = (view, item, view._expires_at)
item.refresh_state(interaction)
view.dispatch(self._state, item, interaction)
def is_message_tracked(self, message_id: int):
return message_id in self._synced_message_views
def remove_message_tracking(self, message_id: int) -> Optional[View]:
return self._synced_message_views.pop(message_id, None)
def update_from_message(self, message_id: int, components: List[ComponentPayload]):
# pre-req: is_message_tracked == true
view = self._synced_message_views[message_id]
view.refresh([_component_factory(d) for d in components])

View File

@@ -22,10 +22,11 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from typing import Optional, TYPE_CHECKING
import discord.abc
from .flags import PublicUserFlags
from .utils import snowflake_time, _bytes_to_base64_data
from .enums import DefaultAvatar, try_enum
from .enums import DefaultAvatar
from .colour import Colour
from .asset import Asset
@@ -36,15 +37,29 @@ __all__ = (
_BaseUser = discord.abc.User
class BaseUser(_BaseUser):
__slots__ = ('name', 'id', 'discriminator', 'avatar', 'bot', 'system', '_public_flags', '_state')
__slots__ = ('name', 'id', 'discriminator', '_avatar', 'bot', 'system', '_public_flags', '_state')
if TYPE_CHECKING:
name: str
id: int
discriminator: str
bot: bool
system: bool
def __init__(self, *, state, data):
self._state = state
self._update(data)
def __repr__(self):
return (
f"<BaseUser id={self.id} name={self.name!r} discriminator={self.discriminator!r}"
f" bot={self.bot} system={self.system}>"
)
def __str__(self):
return '{0.name}#{0.discriminator}'.format(self)
return f'{self.name}#{self.discriminator}'
def __eq__(self, other):
return isinstance(other, _BaseUser) and other.id == self.id
@@ -59,19 +74,19 @@ class BaseUser(_BaseUser):
self.name = data['username']
self.id = int(data['id'])
self.discriminator = data['discriminator']
self.avatar = data['avatar']
self._avatar = data['avatar']
self._public_flags = data.get('public_flags', 0)
self.bot = data.get('bot', False)
self.system = data.get('system', False)
@classmethod
def _copy(cls, user):
self = cls.__new__(cls) # bypass __init__
self = cls.__new__(cls) # bypass __init__
self.name = user.name
self.id = user.id
self.discriminator = user.discriminator
self.avatar = user.avatar
self._avatar = user._avatar
self.bot = user.bot
self._state = user._state
self._public_flags = user._public_flags
@@ -82,7 +97,7 @@ class BaseUser(_BaseUser):
return {
'username': self.name,
'id': self.id,
'avatar': self.avatar,
'avatar': self._avatar,
'discriminator': self.discriminator,
'bot': self.bot,
}
@@ -93,66 +108,20 @@ class BaseUser(_BaseUser):
return PublicUserFlags._from_value(self._public_flags)
@property
def avatar_url(self):
def avatar(self):
""":class:`Asset`: Returns an :class:`Asset` for the avatar the user has.
If the user does not have a traditional avatar, an asset for
the default avatar is returned instead.
This is equivalent to calling :meth:`avatar_url_as` with
the default parameters (i.e. webp/gif detection and a size of 1024).
"""
return self.avatar_url_as(format=None, size=1024)
def is_avatar_animated(self):
""":class:`bool`: Indicates if the user has an animated avatar."""
return bool(self.avatar and self.avatar.startswith('a_'))
def avatar_url_as(self, *, format=None, static_format='webp', size=1024):
"""Returns an :class:`Asset` for the avatar the user has.
If the user does not have a traditional avatar, an asset for
the default avatar is returned instead.
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif', and
'gif' is only valid for animated avatars. The size must be a power of 2
between 16 and 4096.
Parameters
-----------
format: Optional[:class:`str`]
The format to attempt to convert the avatar to.
If the format is ``None``, then it is automatically
detected into either 'gif' or static_format depending on the
avatar being animated or not.
static_format: Optional[:class:`str`]
Format to attempt to convert only non-animated avatars to.
Defaults to 'webp'
size: :class:`int`
The size of the image to display.
Raises
------
InvalidArgument
Bad image format passed to ``format`` or ``static_format``, or
invalid ``size``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_avatar(self._state, self, format=format, static_format=static_format, size=size)
if self._avatar is None:
return Asset._from_default_avatar(self._state, int(self.discriminator) % len(DefaultAvatar))
else:
return Asset._from_avatar(self._state, self.id, self._avatar)
@property
def default_avatar(self):
""":class:`DefaultAvatar`: Returns the default avatar for a given user. This is calculated by the user's discriminator."""
return try_enum(DefaultAvatar, int(self.discriminator) % len(DefaultAvatar))
@property
def default_avatar_url(self):
""":class:`Asset`: Returns a URL for a user's default avatar."""
return Asset(self._state, f'/embed/avatars/{self.default_avatar.value}.png')
""":class:`Asset`: Returns the default avatar for a given user. This is calculated by the user's discriminator."""
return Asset._from_default_avatar(self._state, int(self.discriminator) % len(DefaultAvatar))
@property
def colour(self):
@@ -177,22 +146,6 @@ class BaseUser(_BaseUser):
""":class:`str`: Returns a string that allows you to mention the given user."""
return f'<@{self.id}>'
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.
"""
return channel.permissions_for(self)
@property
def created_at(self):
""":class:`datetime.datetime`: Returns the user's creation time in UTC.
@@ -230,6 +183,7 @@ class BaseUser(_BaseUser):
return any(user.id == self.id for user in message.mentions)
class ClientUser(BaseUser):
"""Represents your Discord user.
@@ -259,8 +213,6 @@ class ClientUser(BaseUser):
The user's unique ID.
discriminator: :class:`str`
The user's discriminator. This is given when the username has conflicts.
avatar: Optional[:class:`str`]
The avatar hash the user has. Could be ``None``.
bot: :class:`bool`
Specifies if the user is a bot account.
system: :class:`bool`
@@ -269,21 +221,23 @@ class ClientUser(BaseUser):
.. versionadded:: 1.3
verified: :class:`bool`
Specifies if the user is a verified account.
Specifies if the user's email is verified.
locale: Optional[:class:`str`]
The IETF language tag used to identify the language the user is using.
mfa_enabled: :class:`bool`
Specifies if the user has MFA turned on and working.
"""
__slots__ = BaseUser.__slots__ + \
('locale', '_flags', 'verified', 'mfa_enabled', '__weakref__')
__slots__ = BaseUser.__slots__ + ('locale', '_flags', 'verified', 'mfa_enabled', '__weakref__')
def __init__(self, *, state, data):
super().__init__(state=state, data=data)
def __repr__(self):
return '<ClientUser id={0.id} name={0.name!r} discriminator={0.discriminator!r}' \
' bot={0.bot} verified={0.verified} mfa_enabled={0.mfa_enabled}>'.format(self)
return (
f'<ClientUser id={self.id} name={self.name!r} discriminator={self.discriminator!r}'
f' bot={self.bot} verified={self.verified} mfa_enabled={self.mfa_enabled}>'
)
def _update(self, data):
super()._update(data)
@@ -293,8 +247,7 @@ class ClientUser(BaseUser):
self._flags = data.get('flags', 0)
self.mfa_enabled = data.get('mfa_enabled', False)
async def edit(self, *, username=None, avatar=None):
async def edit(self, *, username: str = None, avatar: Optional[bytes] = None) -> None:
"""|coro|
Edits the current profile of the client.
@@ -330,6 +283,7 @@ class ClientUser(BaseUser):
data = await self._state.http.edit_profile(username=username, avatar=avatar)
self._update(data)
class User(BaseUser, discord.abc.Messageable):
"""Represents a Discord user.
@@ -359,8 +313,6 @@ class User(BaseUser, discord.abc.Messageable):
The user's unique ID.
discriminator: :class:`str`
The user's discriminator. This is given when the username has conflicts.
avatar: Optional[:class:`str`]
The avatar hash the user has. Could be None.
bot: :class:`bool`
Specifies if the user is a bot account.
system: :class:`bool`
@@ -370,7 +322,7 @@ class User(BaseUser, discord.abc.Messageable):
__slots__ = BaseUser.__slots__ + ('__weakref__',)
def __repr__(self):
return '<User id={0.id} name={0.name!r} discriminator={0.discriminator!r} bot={0.bot}>'.format(self)
return f'<User id={self.id} name={self.name!r} discriminator={self.discriminator!r} bot={self.bot}>'
async def _get_channel(self):
ch = await self.create_dm()

View File

@@ -21,11 +21,33 @@ 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 array
import asyncio
import collections.abc
from typing import Optional, overload
from typing import (
Any,
AsyncIterator,
Callable,
Dict,
ForwardRef,
Generic,
Iterable,
Iterator,
List,
Literal,
Mapping,
Optional,
Protocol,
Sequence,
Tuple,
Type,
TypeVar,
Union,
overload,
TYPE_CHECKING,
)
import unicodedata
from base64 import b64encode
from bisect import bisect_left
@@ -35,12 +57,14 @@ from inspect import isawaitable as _isawaitable, signature as _signature
from operator import attrgetter
import json
import re
import sys
import types
import warnings
from .errors import InvalidArgument
__all__ = (
'oauth_uri',
'oauth_url',
'snowflake_time',
'time_snowflake',
'find',
@@ -50,10 +74,27 @@ __all__ = (
'remove_markdown',
'escape_markdown',
'escape_mentions',
'as_chunks',
)
DISCORD_EPOCH = 1420070400000
class cached_property:
class _MissingSentinel:
def __eq__(self, other):
return False
def __bool__(self):
return False
def __repr__(self):
return '...'
MISSING: Any = _MissingSentinel()
class _cached_property:
def __init__(self, function):
self.function = function
self.__doc__ = getattr(function, '__doc__')
@@ -67,13 +108,42 @@ class cached_property:
return value
class CachedSlotProperty:
def __init__(self, name, function):
if TYPE_CHECKING:
from functools import cached_property as cached_property
from .permissions import Permissions
from .abc import Snowflake
from .invite import Invite
from .template import Template
class _RequestLike(Protocol):
headers: Mapping[str, Any]
else:
cached_property = _cached_property
T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
_Iter = Union[Iterator[T], AsyncIterator[T]]
class CachedSlotProperty(Generic[T, T_co]):
def __init__(self, name: str, function: Callable[[T], T_co]) -> None:
self.name = name
self.function = function
self.__doc__ = getattr(function, '__doc__')
def __get__(self, instance, owner):
@overload
def __get__(self, instance: None, owner: Type[T]) -> CachedSlotProperty[T, T_co]:
...
@overload
def __get__(self, instance: T, owner: Type[T]) -> T_co:
...
def __get__(self, instance: Optional[T], owner: Type[T]) -> Any:
if instance is None:
return self
@@ -84,74 +154,109 @@ class CachedSlotProperty:
setattr(instance, self.name, value)
return value
def cached_slot_property(name):
def decorator(func):
class classproperty(Generic[T_co]):
def __init__(self, fget: Callable[[Any], T_co]) -> None:
self.fget = fget
def __get__(self, instance: Optional[Any], owner: Type[Any]) -> T_co:
return self.fget(owner)
def __set__(self, instance, value) -> None:
raise AttributeError('cannot set attribute')
def cached_slot_property(name: str) -> Callable[[Callable[[T], T_co]], CachedSlotProperty[T, T_co]]:
def decorator(func: Callable[[T], T_co]) -> CachedSlotProperty[T, T_co]:
return CachedSlotProperty(name, func)
return decorator
class SequenceProxy(collections.abc.Sequence):
class SequenceProxy(Generic[T_co], collections.abc.Sequence):
"""Read-only proxy of a Sequence."""
def __init__(self, proxied):
def __init__(self, proxied: Sequence[T_co]):
self.__proxied = proxied
def __getitem__(self, idx):
def __getitem__(self, idx: int) -> T_co:
return self.__proxied[idx]
def __len__(self):
def __len__(self) -> int:
return len(self.__proxied)
def __contains__(self, item):
def __contains__(self, item: Any) -> bool:
return item in self.__proxied
def __iter__(self):
def __iter__(self) -> Iterator[T_co]:
return iter(self.__proxied)
def __reversed__(self):
def __reversed__(self) -> Iterator[T_co]:
return reversed(self.__proxied)
def index(self, value, *args, **kwargs):
def index(self, value: Any, *args, **kwargs) -> int:
return self.__proxied.index(value, *args, **kwargs)
def count(self, value):
def count(self, value: Any) -> int:
return self.__proxied.count(value)
@overload
def parse_time(timestamp: None) -> None:
...
@overload
def parse_time(timestamp: str) -> datetime.datetime:
...
@overload
def parse_time(timestamp: Optional[str]) -> Optional[datetime.datetime]:
...
def parse_time(timestamp: Optional[str]) -> Optional[datetime.datetime]:
if timestamp:
return datetime.datetime.fromisoformat(timestamp)
return None
def copy_doc(original):
def decorator(overriden):
def copy_doc(original: Callable[..., Any]) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
def decorator(overriden: Callable[..., Any]) -> Callable[..., Any]:
overriden.__doc__ = original.__doc__
overriden.__signature__ = _signature(original)
overriden.__signature__ = _signature(original) # type: ignore
return overriden
return decorator
def deprecated(instead=None):
def actual_decorator(func):
def deprecated(instead: Optional[str] = None) -> Callable[[Callable[..., T]], Callable[..., T]]:
def actual_decorator(func: Callable[..., T]) -> Callable[..., T]:
@functools.wraps(func)
def decorated(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning) # turn off filter
def decorated(*args, **kwargs) -> T:
warnings.simplefilter('always', DeprecationWarning) # turn off filter
if instead:
fmt = "{0.__name__} is deprecated, use {1} instead."
else:
fmt = '{0.__name__} is deprecated.'
warnings.warn(fmt.format(func, instead), stacklevel=3, category=DeprecationWarning)
warnings.simplefilter('default', DeprecationWarning) # reset filter
warnings.simplefilter('default', DeprecationWarning) # reset filter
return func(*args, **kwargs)
return decorated
return actual_decorator
def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None, scopes=None):
def oauth_url(
client_id: str,
permissions: Optional[Permissions] = None,
guild: Optional[Snowflake] = None,
redirect_uri: Optional[str] = None,
scopes: Optional[Iterable[str]] = None,
):
"""A helper function that returns the OAuth2 URL for inviting the bot
into guilds.
@@ -162,7 +267,7 @@ def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None, scopes
permissions: :class:`~discord.Permissions`
The permissions you're requesting. If not given then you won't be requesting any
permissions.
guild: :class:`~discord.Guild`
guild: :class:`~discord.abc.Snowflake`
The guild to pre-select in the authorization screen, if available.
redirect_uri: :class:`str`
An optional valid redirect URI.
@@ -184,6 +289,7 @@ def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None, scopes
url = url + "&guild_id=" + str(guild.id)
if redirect_uri is not None:
from urllib.parse import urlencode
url = url + "&response_type=code&" + urlencode({'redirect_uri': redirect_uri})
return url
@@ -203,6 +309,7 @@ def snowflake_time(id: int) -> datetime.datetime:
timestamp = ((id >> 22) + DISCORD_EPOCH) / 1000
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
def time_snowflake(dt: datetime.datetime, high: bool = False) -> int:
"""Returns a numeric snowflake pretending to be created at the given date.
@@ -226,9 +333,10 @@ def time_snowflake(dt: datetime.datetime, high: bool = False) -> int:
The snowflake representing the time given.
"""
discord_millis = int(dt.timestamp() * 1000 - DISCORD_EPOCH)
return (discord_millis << 22) + (2**22-1 if high else 0)
return (discord_millis << 22) + (2 ** 22 - 1 if high else 0)
def find(predicate, seq):
def find(predicate: Callable[[T], Any], seq: Iterable[T]) -> Optional[T]:
"""A helper to return the first element found in the sequence
that meets the predicate. For example: ::
@@ -244,7 +352,7 @@ def find(predicate, seq):
-----------
predicate
A function that returns a boolean-like result.
seq: iterable
seq: :class:`collections.abc.Iterable`
The iterable to search through.
"""
@@ -253,7 +361,8 @@ def find(predicate, seq):
return element
return None
def get(iterable, **attrs):
def get(iterable: Iterable[T], **attrs: Any) -> Optional[T]:
r"""A helper that returns the first element in the iterable that meets
all the traits passed in ``attrs``. This is an alternative for
:func:`~discord.utils.find`.
@@ -310,22 +419,19 @@ def get(iterable, **attrs):
return elem
return None
converted = [
(attrget(attr.replace('__', '.')), value)
for attr, value in attrs.items()
]
converted = [(attrget(attr.replace('__', '.')), value) for attr, value in attrs.items()]
for elem in iterable:
if _all(pred(elem) == value for pred, value in converted):
return elem
return None
def _unique(iterable):
seen = set()
adder = seen.add
return [x for x in iterable if not (x in seen or adder(x))]
def _get_as_snowflake(data, key):
def _unique(iterable: Iterable[T]) -> List[T]:
return [x for x in dict.fromkeys(iterable)]
def _get_as_snowflake(data: Any, key: str) -> Optional[int]:
try:
value = data[key]
except KeyError:
@@ -333,7 +439,8 @@ def _get_as_snowflake(data, key):
else:
return value and int(value)
def _get_mime_type_for_image(data):
def _get_mime_type_for_image(data: bytes):
if data.startswith(b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'):
return 'image/png'
elif data[0:3] == b'\xff\xd8\xff' or data[6:10] in (b'JFIF', b'Exif'):
@@ -345,17 +452,20 @@ def _get_mime_type_for_image(data):
else:
raise InvalidArgument('Unsupported image type given')
def _bytes_to_base64_data(data):
def _bytes_to_base64_data(data: bytes) -> str:
fmt = 'data:{mime};base64,{data}'
mime = _get_mime_type_for_image(data)
b64 = b64encode(data).decode('ascii')
return fmt.format(mime=mime, data=b64)
def to_json(obj):
def to_json(obj: Any) -> str:
return json.dumps(obj, separators=(',', ':'), ensure_ascii=True)
def _parse_ratelimit_header(request, *, use_clock=False):
reset_after = request.headers.get('X-Ratelimit-Reset-After')
def _parse_ratelimit_header(request: Any, *, use_clock: bool = False) -> float:
reset_after: Optional[str] = request.headers.get('X-Ratelimit-Reset-After')
if use_clock or not reset_after:
utc = datetime.timezone.utc
now = datetime.datetime.now(utc)
@@ -364,6 +474,7 @@ def _parse_ratelimit_header(request, *, use_clock=False):
else:
return float(reset_after)
async def maybe_coroutine(f, *args, **kwargs):
value = f(*args, **kwargs)
if _isawaitable(value):
@@ -371,6 +482,7 @@ async def maybe_coroutine(f, *args, **kwargs):
else:
return value
async def async_all(gen, *, check=_isawaitable):
for elem in gen:
if check(elem):
@@ -379,10 +491,9 @@ async def async_all(gen, *, check=_isawaitable):
return False
return True
async def sane_wait_for(futures, *, timeout):
ensured = [
asyncio.ensure_future(fut) for fut in futures
]
ensured = [asyncio.ensure_future(fut) for fut in futures]
done, pending = await asyncio.wait(ensured, timeout=timeout, return_when=asyncio.ALL_COMPLETED)
if len(pending) != 0:
@@ -390,7 +501,23 @@ async def sane_wait_for(futures, *, timeout):
return done
async def sleep_until(when, result=None):
def get_slots(cls: Type[Any]) -> Iterator[str]:
for mro in reversed(cls.__mro__):
try:
yield from mro.__slots__
except AttributeError:
continue
def compute_timedelta(dt: datetime.datetime):
if dt.tzinfo is None:
dt = dt.astimezone()
now = datetime.datetime.now(datetime.timezone.utc)
return max((dt - now).total_seconds(), 0)
async def sleep_until(when: datetime.datetime, result: Optional[T] = None) -> Optional[T]:
"""|coro|
Sleep until a specified time.
@@ -407,16 +534,14 @@ async def sleep_until(when, result=None):
result: Any
If provided is returned to the caller when the coroutine completes.
"""
if when.tzinfo is None:
when = when.astimezone()
now = datetime.datetime.now(datetime.timezone.utc)
delta = (when - now).total_seconds()
return await asyncio.sleep(max(delta, 0), result)
delta = compute_timedelta(when)
return await asyncio.sleep(delta, result)
def utcnow() -> datetime.datetime:
"""A helper function to return an aware UTC datetime representing the current time.
This should be preferred to :func:`datetime.datetime.utcnow` since it is an aware
This should be preferred to :meth:`datetime.datetime.utcnow` since it is an aware
datetime, compared to the naive datetime in the standard library.
.. versionadded:: 2.0
@@ -428,9 +553,11 @@ def utcnow() -> datetime.datetime:
"""
return datetime.datetime.now(datetime.timezone.utc)
def valid_icon_size(size):
def valid_icon_size(size: int) -> bool:
"""Icons must be power of 2 within [16, 4096]."""
return not size & (size - 1) and size in range(16, 4097)
return not size & (size - 1) and 4096 >= size >= 16
class SnowflakeList(array.array):
"""Internal data storage class to efficiently store a list of snowflakes.
@@ -446,24 +573,31 @@ class SnowflakeList(array.array):
__slots__ = ()
def __new__(cls, data, *, is_sorted=False):
return array.array.__new__(cls, 'Q', data if is_sorted else sorted(data))
if TYPE_CHECKING:
def add(self, element):
def __init__(self, data: Iterable[int], *, is_sorted: bool = False):
...
def __new__(cls, data: Iterable[int], *, is_sorted: bool = False):
return array.array.__new__(cls, 'Q', data if is_sorted else sorted(data)) # type: ignore
def add(self, element: int) -> None:
i = bisect_left(self, element)
self.insert(i, element)
def get(self, element):
def get(self, element: int) -> Optional[int]:
i = bisect_left(self, element)
return self[i] if i != len(self) and self[i] == element else None
def has(self, element):
def has(self, element: int) -> bool:
i = bisect_left(self, element)
return i != len(self) and self[i] == element
_IS_ASCII = re.compile(r'^[\x00-\x7f]+$')
def _string_width(string, *, _IS_ASCII=_IS_ASCII):
def _string_width(string: str, *, _IS_ASCII=_IS_ASCII) -> int:
"""Returns string's width."""
match = _IS_ASCII.match(string)
if match:
@@ -473,7 +607,8 @@ def _string_width(string, *, _IS_ASCII=_IS_ASCII):
func = unicodedata.east_asian_width
return sum(2 if func(char) in UNICODE_WIDE_CHAR_TYPE else 1 for char in string)
def resolve_invite(invite):
def resolve_invite(invite: Union[Invite, str]) -> str:
"""
Resolves an invite from a :class:`~discord.Invite`, URL or code.
@@ -488,6 +623,7 @@ def resolve_invite(invite):
The invite code.
"""
from .invite import Invite # circular import
if isinstance(invite, Invite):
return invite.code
else:
@@ -497,7 +633,8 @@ def resolve_invite(invite):
return m.group(1)
return invite
def resolve_template(code):
def resolve_template(code: Union[Template, str]) -> str:
"""
Resolves a template code from a :class:`~discord.Template`, URL or code.
@@ -513,7 +650,8 @@ def resolve_template(code):
:class:`str`
The template code.
"""
from .template import Template # circular import
from .template import Template # circular import
if isinstance(code, Template):
return code.code
else:
@@ -523,8 +661,8 @@ def resolve_template(code):
return m.group(1)
return code
_MARKDOWN_ESCAPE_SUBREGEX = '|'.join(r'\{0}(?=([\s\S]*((?<!\{0})\{0})))'.format(c)
for c in ('*', '`', '_', '~', '|'))
_MARKDOWN_ESCAPE_SUBREGEX = '|'.join(r'\{0}(?=([\s\S]*((?<!\{0})\{0})))'.format(c) for c in ('*', '`', '_', '~', '|'))
_MARKDOWN_ESCAPE_COMMON = r'^>(?:>>)?\s|\[.+\]\(.+\)'
@@ -534,7 +672,8 @@ _URL_REGEX = r'(?P<url><[^: >]+:\/[^ >]+>|(?:https?|steam):\/\/[^\s<]+[^<.,:;\"\
_MARKDOWN_STOCK_REGEX = fr'(?P<markdown>[_\\~|\*`]|{_MARKDOWN_ESCAPE_COMMON})'
def remove_markdown(text, *, ignore_links=True):
def remove_markdown(text: str, *, ignore_links: bool = True) -> str:
"""A helper function that removes markdown characters.
.. versionadded:: 1.7
@@ -567,7 +706,8 @@ def remove_markdown(text, *, ignore_links=True):
regex = f'(?:{_URL_REGEX}|{regex})'
return re.sub(regex, replacement, text, 0, re.MULTILINE)
def escape_markdown(text, *, as_needed=False, ignore_links=True):
def escape_markdown(text: str, *, as_needed: bool = False, ignore_links: bool = True) -> str:
r"""A helper function that escapes Discord's markdown.
Parameters
@@ -593,6 +733,7 @@ def escape_markdown(text, *, as_needed=False, ignore_links=True):
"""
if not as_needed:
def replacement(match):
groupdict = match.groupdict()
is_url = groupdict.get('url')
@@ -608,7 +749,8 @@ def escape_markdown(text, *, as_needed=False, ignore_links=True):
text = re.sub(r'\\', r'\\\\', text)
return _MARKDOWN_ESCAPE_REGEX.sub(r'\\\1', text)
def escape_mentions(text):
def escape_mentions(text: str) -> str:
"""A helper function that escapes everyone, here, role, and user mentions.
.. note::
@@ -632,3 +774,165 @@ def escape_mentions(text):
The text with the mentions removed.
"""
return re.sub(r'@(everyone|here|[!&]?[0-9]{17,20})', '@\u200b\\1', text)
def _chunk(iterator: Iterator[T], max_size: int) -> Iterator[List[T]]:
ret = []
n = 0
for item in iterator:
ret.append(item)
n += 1
if n == max_size:
yield ret
ret = []
n = 0
if ret:
yield ret
async def _achunk(iterator: AsyncIterator[T], max_size: int) -> AsyncIterator[List[T]]:
ret = []
n = 0
async for item in iterator:
ret.append(item)
n += 1
if n == max_size:
yield ret
ret = []
n = 0
if ret:
yield ret
@overload
def as_chunks(iterator: Iterator[T], max_size: int) -> Iterator[List[T]]:
...
@overload
def as_chunks(iterator: AsyncIterator[T], max_size: int) -> AsyncIterator[List[T]]:
...
def as_chunks(iterator: _Iter[T], max_size: int) -> _Iter[List[T]]:
"""A helper function that collects an iterator into chunks of a given size.
.. versionadded:: 2.0
Parameters
----------
iterator: Union[:class:`collections.abc.Iterator`, :class:`collections.abc.AsyncIterator`]
The iterator to chunk, can be sync or async.
max_size: :class:`int`
The maximum chunk size.
.. warning::
The last chunk collected may not be as large as ``max_size``.
Returns
--------
Union[:class:`Iterator`, :class:`AsyncIterator`]
A new iterator which yields chunks of a given size.
"""
if max_size <= 0:
raise ValueError('Chunk sizes must be greater than 0.')
if isinstance(iterator, AsyncIterator):
return _achunk(iterator, max_size)
return _chunk(iterator, max_size)
PY_310 = sys.version_info >= (3, 10)
def flatten_literal_params(parameters: Iterable[Any]) -> Tuple[Any, ...]:
params = []
literal_cls = type(Literal[0])
for p in parameters:
if isinstance(p, literal_cls):
params.extend(p.__args__)
else:
params.append(p)
return tuple(params)
def normalise_optional_params(parameters: Iterable[Any]) -> Tuple[Any, ...]:
none_cls = type(None)
return tuple(p for p in parameters if p is not none_cls) + (none_cls,)
def evaluate_annotation(
tp: Any,
globals: Dict[str, Any],
locals: Dict[str, Any],
cache: Dict[str, Any],
*,
implicit_str: bool = True,
):
if isinstance(tp, ForwardRef):
tp = tp.__forward_arg__
# ForwardRefs always evaluate their internals
implicit_str = True
if implicit_str and isinstance(tp, str):
if tp in cache:
return cache[tp]
evaluated = eval(tp, globals, locals)
cache[tp] = evaluated
return evaluate_annotation(evaluated, globals, locals, cache)
if hasattr(tp, '__args__'):
implicit_str = True
is_literal = False
args = tp.__args__
if not hasattr(tp, '__origin__'):
if PY_310 and tp.__class__ is types.Union:
converted = Union[args] # type: ignore
return evaluate_annotation(converted, globals, locals, cache)
return tp
if tp.__origin__ is Union:
try:
if args.index(type(None)) != len(args) - 1:
args = normalise_optional_params(tp.__args__)
except ValueError:
pass
if tp.__origin__ is Literal:
if not PY_310:
args = flatten_literal_params(tp.__args__)
implicit_str = False
is_literal = True
evaluated_args = tuple(evaluate_annotation(arg, globals, locals, cache, implicit_str=implicit_str) for arg in args)
if is_literal and not all(isinstance(x, (str, int, bool, type(None))) for x in evaluated_args):
raise TypeError('Literal arguments must be of type str, int, bool, or NoneType.')
if evaluated_args == args:
return tp
try:
return tp.copy_with(evaluated_args)
except AttributeError:
return tp.__origin__[evaluated_args]
return tp
def resolve_annotation(
annotation: Any,
globalns: Dict[str, Any],
localns: Optional[Dict[str, Any]],
cache: Optional[Dict[str, Any]],
) -> Any:
if annotation is None:
return type(None)
if isinstance(annotation, str):
annotation = ForwardRef(annotation)
locals = globalns if localns is None else localns
if cache is None:
cache = {}
return evaluate_annotation(annotation, globalns, locals, cache)

View File

@@ -42,6 +42,7 @@ import socket
import logging
import struct
import threading
from typing import Any, Callable
from . import opus, utils
from .backoff import ExponentialBackoff
@@ -71,9 +72,9 @@ class VoiceProtocol:
This class allows you to implement a protocol to allow for an external
method of sending voice, such as Lavalink_ or a native library implementation.
These classes are passed to :meth:`abc.Connectable.connect`.
These classes are passed to :meth:`abc.Connectable.connect <VoiceChannel.connect>`.
.. _Lavalink: https://github.com/Frederikam/Lavalink
.. _Lavalink: https://github.com/freyacodes/Lavalink
Parameters
------------
@@ -121,7 +122,7 @@ class VoiceProtocol:
"""
raise NotImplementedError
async def connect(self, *, timeout, reconnect):
async def connect(self, *, timeout: float, reconnect: bool):
"""|coro|
An abstract method called when the client initiates the connection request.
@@ -144,7 +145,7 @@ class VoiceProtocol:
"""
raise NotImplementedError
async def disconnect(self, *, force):
async def disconnect(self, *, force: bool):
"""|coro|
An abstract method called when the client terminates the connection.
@@ -328,7 +329,7 @@ class VoiceClient(VoiceProtocol):
self._connected.set()
return ws
async def connect(self, *, reconnect, timeout):
async def connect(self, *, reconnect: bool, timeout: bool):
log.info('Connecting to voice...')
self.timeout = timeout
@@ -451,7 +452,7 @@ class VoiceClient(VoiceProtocol):
log.warning('Could not connect to voice... Retrying...')
continue
async def disconnect(self, *, force=False):
async def disconnect(self, *, force: bool = False):
"""|coro|
Disconnects this voice client from voice.
@@ -525,7 +526,7 @@ class VoiceClient(VoiceProtocol):
return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext + nonce[:4]
def play(self, source, *, after=None):
def play(self, source: AudioSource, *, after: Callable[[Exception], Any]=None):
"""Plays an :class:`AudioSource`.
The finalizer, ``after`` is called after the source has been exhausted

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
"""
discord.webhook
~~~~~~~~~~~~~~
Webhook support
:copyright: (c) 2015-present Rapptz
:license: MIT, see LICENSE for more details.
"""
from .async_ import *
from .sync import *

1523
discord/webhook/async_.py Normal file

File diff suppressed because it is too large Load Diff

1038
discord/webhook/sync.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -22,12 +22,24 @@ 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, List, Optional, TYPE_CHECKING, Union
from .utils import snowflake_time, _get_as_snowflake, resolve_invite
from .user import BaseUser
from .activity import create_activity
from .activity import Activity, BaseActivity, Spotify, create_activity
from .invite import Invite
from .enums import Status, try_enum
if TYPE_CHECKING:
import datetime
from .state import ConnectionState
from .types.widget import (
WidgetMember as WidgetMemberPayload,
Widget as WidgetPayload,
)
__all__ = (
'WidgetChannel',
'WidgetMember',
@@ -66,25 +78,24 @@ class WidgetChannel:
"""
__slots__ = ('id', 'name', 'position')
def __init__(self, id: int, name: str, position: int) -> None:
self.id: int = id
self.name: str = name
self.position: int = position
def __init__(self, **kwargs):
self.id = kwargs.pop('id')
self.name = kwargs.pop('name')
self.position = kwargs.pop('position')
def __str__(self):
def __str__(self) -> str:
return self.name
def __repr__(self):
return '<WidgetChannel id={0.id} name={0.name!r} position={0.position!r}>'.format(self)
def __repr__(self) -> str:
return f'<WidgetChannel id={self.id} name={self.name!r} position={self.position!r}>'
@property
def mention(self):
def mention(self) -> str:
""":class:`str`: The string that allows you to mention the channel."""
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)
@@ -133,32 +144,49 @@ class WidgetMember(BaseUser):
Whether the member is currently muted.
suppress: Optional[:class:`bool`]
Whether the member is currently being suppressed.
connected_channel: Optional[:class:`VoiceChannel`]
connected_channel: Optional[:class:`WidgetChannel`]
Which channel the member is connected to.
"""
__slots__ = ('name', 'status', 'nick', 'avatar', 'discriminator',
'id', 'bot', 'activity', 'deafened', 'suppress', 'muted',
'connected_channel')
def __init__(self, *, state, data, connected_channel=None):
if TYPE_CHECKING:
activity: Optional[Union[BaseActivity, Spotify]]
def __init__(
self,
*,
state: ConnectionState,
data: WidgetMemberPayload,
connected_channel: Optional[WidgetChannel] = None
) -> None:
super().__init__(state=state, data=data)
self.nick = data.get('nick')
self.status = try_enum(Status, data.get('status'))
self.deafened = data.get('deaf', False) or data.get('self_deaf', False)
self.muted = data.get('mute', False) or data.get('self_mute', False)
self.suppress = data.get('suppress', False)
self.nick: Optional[str] = data.get('nick')
self.status: Status = try_enum(Status, data.get('status'))
self.deafened: Optional[bool] = data.get('deaf', False) or data.get('self_deaf', False)
self.muted: Optional[bool] = data.get('mute', False) or data.get('self_mute', False)
self.suppress: Optional[bool] = data.get('suppress', False)
try:
game = data['game']
except KeyError:
self.activity = None
activity = None
else:
self.activity = create_activity(game)
activity = create_activity(game)
self.connected_channel = connected_channel
self.activity: Optional[Union[BaseActivity, Spotify]] = activity
self.connected_channel: Optional[WidgetChannel] = connected_channel
def __repr__(self) -> str:
return (
f"<WidgetMember name={self.name!r} discriminator={self.discriminator!r}"
f" bot={self.bot} nick={self.nick!r}>"
)
@property
def display_name(self):
def display_name(self) -> str:
""":class:`str`: Returns the member's display name."""
return self.nick or self.name
@@ -185,9 +213,9 @@ class Widget:
The guild's ID.
name: :class:`str`
The guild's name.
channels: Optional[List[:class:`WidgetChannel`]]
channels: List[:class:`WidgetChannel`]
The accessible voice channels in the guild.
members: Optional[List[:class:`Member`]]
members: List[:class:`Member`]
The online members in the server. Offline members
do not appear in the widget.
@@ -201,56 +229,58 @@ class Widget:
"""
__slots__ = ('_state', 'channels', '_invite', 'id', 'members', 'name')
def __init__(self, *, state, data):
def __init__(self, *, state: ConnectionState, data: WidgetPayload) -> None:
self._state = state
self._invite = data['instant_invite']
self.name = data['name']
self.id = int(data['id'])
self.name: str = data['name']
self.id: int = int(data['id'])
self.channels = []
self.channels: List[WidgetChannel] = []
for channel in data.get('channels', []):
_id = int(channel['id'])
self.channels.append(WidgetChannel(id=_id, name=channel['name'], position=channel['position']))
self.members = []
self.members: List[WidgetMember] = []
channels = {channel.id: channel for channel in self.channels}
for member in data.get('members', []):
connected_channel = _get_as_snowflake(member, 'channel_id')
if connected_channel in channels:
connected_channel = channels[connected_channel]
connected_channel = channels[connected_channel] # type: ignore
elif connected_channel:
connected_channel = WidgetChannel(id=connected_channel, name='', position=0)
self.members.append(WidgetMember(state=self._state, data=member, connected_channel=connected_channel))
self.members.append(WidgetMember(state=self._state, data=member, connected_channel=connected_channel)) # type: ignore
def __str__(self):
def __str__(self) -> str:
return self.json_url
def __eq__(self, other):
return self.id == other.id
def __eq__(self, other: Any) -> bool:
if isinstance(other, Widget):
return self.id == other.id
return False
def __repr__(self):
return '<Widget id={0.id} name={0.name!r} invite_url={0.invite_url!r}>'.format(self)
def __repr__(self) -> str:
return f'<Widget id={self.id} name={self.name!r} invite_url={self.invite_url!r}>'
@property
def created_at(self):
def created_at(self) -> datetime.datetime:
""":class:`datetime.datetime`: Returns the member's creation time in UTC."""
return snowflake_time(self.id)
@property
def json_url(self):
def json_url(self) -> str:
""":class:`str`: The JSON URL of the widget."""
return f"https://discord.com/api/guilds/{self.id}/widget.json"
@property
def invite_url(self):
def invite_url(self) -> str:
"""Optional[:class:`str`]: The invite URL for the guild, if available."""
return self._invite
async def fetch_invite(self, *, with_counts=True):
async def fetch_invite(self, *, with_counts: bool = True) -> Invite:
"""|coro|
Retrieves an :class:`Invite` from a invite URL or ID.
Retrieves an :class:`Invite` from the widget's invite URL.
This is the same as :meth:`Client.fetch_invite`; the invite
code is abstracted away.
@@ -264,9 +294,8 @@ class Widget:
Returns
--------
:class:`Invite`
The invite from the URL/ID.
The invite from the widget's invite URL.
"""
if self._invite:
invite_id = resolve_invite(self._invite)
data = await self._state.http.get_invite(invite_id, with_counts=with_counts)
return Invite.from_incomplete(state=self._state, data=data)
invite_id = resolve_invite(self._invite)
data = await self._state.http.get_invite(invite_id, with_counts=with_counts)
return Invite.from_incomplete(state=self._state, data=data)

View File

@@ -894,11 +894,15 @@ dl.field-list {
display: block;
}
/* internal references are forced to bold for some reason */
a.reference.internal > strong {
/* cross-references are forced to bold for some reason */
a.reference > strong {
font-weight: unset;
font-family: var(--monospace-font-family);
}
a.reference.pep > strong,
a.reference.rfc > strong {
font-family: inherit;
}
/* exception hierarchy */

File diff suppressed because it is too large Load Diff

View File

@@ -40,6 +40,7 @@ extensions = [
'exception_hierarchy',
'attributetable',
'resourcelinks',
'nitpick_file_ignorer',
]
autodoc_member_order = 'bysource'
@@ -53,7 +54,7 @@ extlinks = {
intersphinx_mapping = {
'py': ('https://docs.python.org/3', None),
'aio': ('https://docs.aiohttp.org/en/stable/', None),
'req': ('http://docs.python-requests.org/en/latest/', 'requests.inv')
'req': ('https://docs.python-requests.org/en/latest/', None)
}
rst_prolog = """
@@ -140,6 +141,13 @@ pygments_style = 'friendly'
#keep_warnings = False
# Nitpicky mode options
nitpick_ignore_files = [
"migrating_to_async",
"migrating",
"whats_new",
]
# -- Options for HTML output ----------------------------------------------
html_experimental_html5_writer = True

View File

@@ -1,3 +1,5 @@
:orphan:
.. _discord-intro:
Creating a Bot Account
@@ -40,7 +42,7 @@ Creating a Bot account is a pretty straightforward process.
.. warning::
It should be worth noting that this token is essentially your bot's
password. You should **never** share this to someone else. In doing so,
password. You should **never** share this with someone else. In doing so,
someone can log in to your bot and do malicious things, such as leaving
servers, ban all members inside a server, or pinging everyone maliciously.

View File

@@ -42,7 +42,7 @@ Event Reference
These events function similar to :ref:`the regular events <discord-api-events>`, except they
are custom to the command extension module.
.. function:: on_command_error(ctx, error)
.. function:: discord.ext.commands.on_command_error(ctx, error)
An error handler that is called when an error is raised
inside a command either through user input error, check
@@ -55,7 +55,7 @@ are custom to the command extension module.
:param error: The error that was raised.
:type error: :class:`.CommandError` derived
.. function:: on_command(ctx)
.. function:: discord.ext.commands.on_command(ctx)
An event that is called when a command is found and is about to be invoked.
@@ -65,7 +65,7 @@ are custom to the command extension module.
:param ctx: The invocation context.
:type ctx: :class:`.Context`
.. function:: on_command_completion(ctx)
.. function:: discord.ext.commands.on_command_completion(ctx)
An event that is called when a command has completed its invocation.
@@ -176,7 +176,8 @@ Paginator
Enums
------
.. class:: discord.ext.commands.BucketType
.. class:: BucketType
:module: discord.ext.commands
Specifies a type of bucket for, e.g. a cooldown.
@@ -272,6 +273,9 @@ Converters
.. autoclass:: discord.ext.commands.Converter
:members:
.. autoclass:: discord.ext.commands.ObjectConverter
:members:
.. autoclass:: discord.ext.commands.MemberConverter
:members:
@@ -284,6 +288,9 @@ Converters
.. autoclass:: discord.ext.commands.PartialMessageConverter
:members:
.. autoclass:: discord.ext.commands.GuildChannelConverter
:members:
.. autoclass:: discord.ext.commands.TextChannelConverter
:members:
@@ -293,6 +300,9 @@ Converters
.. autoclass:: discord.ext.commands.StoreChannelConverter
:members:
.. autoclass:: discord.ext.commands.StageChannelConverter
:members:
.. autoclass:: discord.ext.commands.CategoryChannelConverter
:members:
@@ -320,27 +330,20 @@ Converters
.. autoclass:: discord.ext.commands.clean_content
:members:
.. data:: ext.commands.Greedy
.. autoclass:: discord.ext.commands.Greedy()
A special converter that greedily consumes arguments until it can't.
As a consequence of this behaviour, most input errors are silently discarded,
since it is used as an indicator of when to stop parsing.
.. autofunction:: discord.ext.commands.run_converters
When a parser error is met the greedy converter stops converting, undoes the
internal string parsing routine, and continues parsing regularly.
Flag Converter
~~~~~~~~~~~~~~~
For example, in the following code:
.. autoclass:: discord.ext.commands.FlagConverter
:members:
.. code-block:: python3
.. autoclass:: discord.ext.commands.Flag()
:members:
@commands.command()
async def test(ctx, numbers: Greedy[int], reason: str):
await ctx.send("numbers: {}, reason: {}".format(numbers, reason))
An invocation of ``[p]test 1 2 3 4 5 6 hello`` would pass ``numbers`` with
``[1, 2, 3, 4, 5, 6]`` and ``reason`` with ``hello``\.
For more information, check :ref:`ext_commands_special_converters`.
.. autofunction:: discord.ext.commands.flag
.. _ext_commands_api_errors:
@@ -374,6 +377,9 @@ Exceptions
.. autoexception:: discord.ext.commands.BadUnionArgument
:members:
.. autoexception:: discord.ext.commands.BadLiteralArgument
:members:
.. autoexception:: discord.ext.commands.PrivateMessageOnly
:members:
@@ -467,6 +473,21 @@ Exceptions
.. autoexception:: discord.ext.commands.NSFWChannelRequired
:members:
.. autoexception:: discord.ext.commands.FlagError
:members:
.. autoexception:: discord.ext.commands.BadFlagArgument
:members:
.. autoexception:: discord.ext.commands.MissingFlagArgument
:members:
.. autoexception:: discord.ext.commands.TooManyFlags
:members:
.. autoexception:: discord.ext.commands.MissingRequiredFlag
:members:
.. autoexception:: discord.ext.commands.ExtensionError
:members:
@@ -512,7 +533,13 @@ Exception Hierarchy
- :exc:`~.commands.EmojiNotFound`
- :exc:`~.commands.PartialEmojiConversionFailure`
- :exc:`~.commands.BadBoolArgument`
- :exc:`~.commands.FlagError`
- :exc:`~.commands.BadFlagArgument`
- :exc:`~.commands.MissingFlagArgument`
- :exc:`~.commands.TooManyFlags`
- :exc:`~.commands.MissingRequiredFlag`
- :exc:`~.commands.BadUnionArgument`
- :exc:`~.commands.BadLiteralArgument`
- :exc:`~.commands.ArgumentParsingError`
- :exc:`~.commands.UnexpectedQuoteError`
- :exc:`~.commands.InvalidEndOfQuotedStringError`

View File

@@ -33,16 +33,16 @@ This example cog defines a ``Greetings`` category for your commands, with a sing
async def on_member_join(self, member):
channel = member.guild.system_channel
if channel is not None:
await channel.send('Welcome {0.mention}.'.format(member))
await channel.send(f'Welcome {member.mention}.')
@commands.command()
async def hello(self, ctx, *, member: discord.Member = None):
"""Says hello"""
member = member or ctx.author
if self._last_member is None or self._last_member.id != member.id:
await ctx.send('Hello {0.name}~'.format(member))
await ctx.send(f'Hello {member.name}~')
else:
await ctx.send('Hello {0.name}... This feels familiar.'.format(member))
await ctx.send(f'Hello {member.name}... This feels familiar.')
self._last_member = member
A couple of technical notes to take into consideration:

View File

@@ -99,7 +99,7 @@ Since positional arguments are just regular Python arguments, you can have as ma
@bot.command()
async def test(ctx, arg1, arg2):
await ctx.send('You passed {} and {}'.format(arg1, arg2))
await ctx.send(f'You passed {arg1} and {arg2}')
Variable
++++++++++
@@ -111,7 +111,8 @@ similar to how variable list parameters are done in Python:
@bot.command()
async def test(ctx, *args):
await ctx.send('{} arguments: {}'.format(len(args), ', '.join(args)))
arguments = ', '.join(args)
await ctx.send(f'{len(args)} arguments: {arguments}')
This allows our user to accept either one or many arguments as they please. This works similar to positional arguments,
so multi-word parameters should be quoted.
@@ -256,7 +257,7 @@ An example converter:
class Slapper(commands.Converter):
async def convert(self, ctx, argument):
to_slap = random.choice(ctx.guild.members)
return '{0.author} slapped {1} because *{2}*'.format(ctx, to_slap, argument)
return f'{ctx.author} slapped {to_slap} because *{argument}*'
@bot.command()
async def slap(ctx, *, reason: Slapper):
@@ -327,7 +328,7 @@ For example, a common idiom would be to have a class and a converter for that cl
else:
await ctx.send("Hm you're not so new.")
This can get tedious, so an inline advanced converter is possible through a ``classmethod`` inside the type:
This can get tedious, so an inline advanced converter is possible through a :func:`classmethod` inside the type:
.. code-block:: python3
@@ -365,7 +366,7 @@ For example, to receive a :class:`Member` you can just pass it as a converter:
@bot.command()
async def joined(ctx, *, member: discord.Member):
await ctx.send('{0} joined on {0.joined_at}'.format(member))
await ctx.send(f'{member} joined on {member.joined_at}')
When this command is executed, it attempts to convert the string given into a :class:`Member` and then passes it as a
parameter for the function. This works by checking if the string is a mention, an ID, a nickname, a username + discriminator,
@@ -373,12 +374,15 @@ or just a regular username. The default set of converters have been written to b
A lot of discord models work out of the gate as a parameter:
- :class:`Object` (since v2.0)
- :class:`Member`
- :class:`User`
- :class:`Message` (since v1.1)
- :class:`PartialMessage` (since v1.7)
- :class:`abc.GuildChannel` (since 2.0)
- :class:`TextChannel`
- :class:`VoiceChannel`
- :class:`StageChannel` (since v1.7)
- :class:`StoreChannel` (since v1.7)
- :class:`CategoryChannel`
- :class:`Invite`
@@ -398,6 +402,8 @@ converter is given below:
+--------------------------+-------------------------------------------------+
| Discord Class | Converter |
+--------------------------+-------------------------------------------------+
| :class:`Object` | :class:`~ext.commands.ObjectConverter` |
+--------------------------+-------------------------------------------------+
| :class:`Member` | :class:`~ext.commands.MemberConverter` |
+--------------------------+-------------------------------------------------+
| :class:`User` | :class:`~ext.commands.UserConverter` |
@@ -406,10 +412,14 @@ converter is given below:
+--------------------------+-------------------------------------------------+
| :class:`PartialMessage` | :class:`~ext.commands.PartialMessageConverter` |
+--------------------------+-------------------------------------------------+
| :class:`.GuildChannel` | :class:`~ext.commands.GuildChannelConverter` |
+--------------------------+-------------------------------------------------+
| :class:`TextChannel` | :class:`~ext.commands.TextChannelConverter` |
+--------------------------+-------------------------------------------------+
| :class:`VoiceChannel` | :class:`~ext.commands.VoiceChannelConverter` |
+--------------------------+-------------------------------------------------+
| :class:`StageChannel` | :class:`~ext.commands.StageChannelConverter` |
+--------------------------+-------------------------------------------------+
| :class:`StoreChannel` | :class:`~ext.commands.StoreChannelConverter` |
+--------------------------+-------------------------------------------------+
| :class:`CategoryChannel` | :class:`~ext.commands.CategoryChannelConverter` |
@@ -489,7 +499,7 @@ Consider the following example:
@bot.command()
async def bottles(ctx, amount: typing.Optional[int] = 99, *, liquid="beer"):
await ctx.send('{} bottles of {} on the wall!'.format(amount, liquid))
await ctx.send(f'{amount} bottles of {liquid} on the wall!')
.. image:: /images/commands/optional1.png
@@ -501,10 +511,31 @@ resumes handling, which in this case would be to pass it into the ``liquid`` par
This converter only works in regular positional parameters, not variable parameters or keyword-only parameters.
typing.Literal
^^^^^^^^^^^^^^^^
A :data:`typing.Literal` is a special type hint that requires the passed parameter to be equal to one of the listed values
after being converted to the same type. For example, given the following:
.. code-block:: python3
from typing import Literal
@bot.command()
async def shop(ctx, buy_sell: Literal['buy', 'sell'], amount: Literal[1, 2], *, item: str):
await ctx.send(f'{buy_sell.capitalize()}ing {amount} {item}(s)!')
The ``buy_sell`` parameter must be either the literal string ``"buy"`` or ``"sell"`` and ``amount`` must convert to the
``int`` ``1`` or ``2``. If ``buy_sell`` or ``amount`` don't match any value, then a special error is raised,
:exc:`~.ext.commands.BadLiteralArgument`. Any literal values can be mixed and matched within the same :data:`typing.Literal` converter.
Note that ``typing.Literal[True]`` and ``typing.Literal[False]`` still follow the :class:`bool` converter rules.
Greedy
^^^^^^^^
The :data:`~ext.commands.Greedy` converter is a generalisation of the :data:`typing.Optional` converter, except applied
The :class:`~ext.commands.Greedy` converter is a generalisation of the :data:`typing.Optional` converter, except applied
to a list of arguments. In simple terms, this means that it tries to convert as much as it can until it can't convert
any further.
@@ -515,7 +546,7 @@ Consider the following example:
@bot.command()
async def slap(ctx, members: commands.Greedy[discord.Member], *, reason='no reason'):
slapped = ", ".join(x.name for x in members)
await ctx.send('{} just got slapped for {}'.format(slapped, reason))
await ctx.send(f'{slapped} just got slapped for {reason}')
When invoked, it allows for any number of members to be passed in:
@@ -525,9 +556,9 @@ The type passed when using this converter depends on the parameter type that it
- Positional parameter types will receive either the default parameter or a :class:`list` of the converted values.
- Variable parameter types will be a :class:`tuple` as usual.
- Keyword-only parameter types will be the same as if :data:`~ext.commands.Greedy` was not passed at all.
- Keyword-only parameter types will be the same as if :class:`~ext.commands.Greedy` was not passed at all.
:data:`~ext.commands.Greedy` parameters can also be made optional by specifying an optional value.
:class:`~ext.commands.Greedy` parameters can also be made optional by specifying an optional value.
When mixed with the :data:`typing.Optional` converter you can provide simple and expressive command invocation syntaxes:
@@ -554,7 +585,7 @@ This command can be invoked any of the following ways:
.. warning::
The usage of :data:`~ext.commands.Greedy` and :data:`typing.Optional` are powerful and useful, however as a
The usage of :class:`~ext.commands.Greedy` and :data:`typing.Optional` are powerful and useful, however as a
price, they open you up to some parsing ambiguities that might surprise some people.
For example, a signature expecting a :data:`typing.Optional` of a :class:`discord.Member` followed by a
@@ -564,7 +595,165 @@ This command can be invoked any of the following ways:
allowed through custom converters or reordering the parameters to minimise clashes.
To help aid with some parsing ambiguities, :class:`str`, ``None``, :data:`typing.Optional` and
:data:`~ext.commands.Greedy` are forbidden as parameters for the :data:`~ext.commands.Greedy` converter.
:class:`~ext.commands.Greedy` are forbidden as parameters for the :class:`~ext.commands.Greedy` converter.
.. _ext_commands_flag_converter:
FlagConverter
++++++++++++++
.. versionadded:: 2.0
A :class:`~ext.commands.FlagConverter` allows the user to specify user-friendly "flags" using :pep:`526` type annotations
or a syntax more reminiscent of the :mod:`py:dataclasses` module.
For example, the following code:
.. code-block:: python3
from discord.ext import commands
import discord
class BanFlags(commands.FlagConverter):
member: discord.Member
reason: str
days: int = 1
@commands.command()
async def ban(ctx, *, flags: BanFlags):
plural = f'{flags.days} days' if flags.days != 1 else f'{flags.days} day'
await ctx.send(f'Banned {flags.member} for {flags.reason!r} (deleted {plural} worth of messages)')
Allows the user to invoke the command using a simple flag-like syntax:
.. image:: /images/commands/flags1.png
Flags use a syntax that allows the user to not require quotes when passing in values to the flag. The goal of the
flag syntax is to be as user-friendly as possible. This makes flags a good choice for complicated commands that can have
multiple knobs to turn or simulating keyword-only parameters in your external command interface. **It is recommended to use
keyword-only parameters with the flag converter**. This ensures proper parsing and behaviour with quoting.
Internally, the :class:`~ext.commands.FlagConverter` class examines the class to find flags. A flag can either be a
class variable with a type annotation or a class variable that's been assigned the result of the :func:`~ext.commands.flag`
function. These flags are then used to define the interface that your users will use. The annotations correspond to
the converters that the flag arguments must adhere to.
For most use cases, no extra work is required to define flags. However, if customisation is needed to control the flag name
or the default value then the :func:`~ext.commands.flag` function can come in handy:
.. code-block:: python3
from typing import List
class BanFlags(commands.FlagConverter):
members: List[discord.Member] = commands.flag(name='member', default=lambda ctx: [])
This tells the parser that the ``members`` attribute is mapped to a flag named ``member`` and that
the default value is an empty list. For greater customisability, the default can either be a value or a callable
that takes the :class:`~ext.commands.Context` as a sole parameter. This callable can either be a function or a coroutine.
In order to customise the flag syntax we also have a few options that can be passed to the class parameter list:
.. code-block:: python3
# --hello world syntax
class PosixLikeFlags(commands.FlagConverter, delimiter=' ', prefix='--'):
hello: str
# /make food
class WindowsLikeFlags(commands.FlagConverter, prefix='/', delimiter=''):
make: str
# TOPIC: not allowed nsfw: yes Slowmode: 100
class Settings(commands.FlagConverter, case_insensitive=True):
topic: Optional[str]
nsfw: Optional[bool]
slowmode: Optional[int]
.. note::
Despite the similarities in these examples to command like arguments, the syntax and parser is not
a command line parser. The syntax is mainly inspired by Discord's search bar input and as a result
all flags need a corresponding value.
The flag converter is similar to regular commands and allows you to use most types of converters
(with the exception of :class:`~ext.commands.Greedy`) as the type annotation. Some extra support is added for specific
annotations as described below.
typing.List
^^^^^^^^^^^^^
If a list is given as a flag annotation it tells the parser that the argument can be passed multiple times.
For example, augmenting the example above:
.. code-block:: python3
from discord.ext import commands
from typing import List
import discord
class BanFlags(commands.FlagConverter):
members: List[discord.Member] = commands.flag(name='member')
reason: str
days: int = 1
@commands.command()
async def ban(ctx, *, flags: BanFlags):
for member in flags.members:
await member.ban(reason=flags.reason, delete_message_days=flags.days)
members = ', '.join(str(member) for member in flags.members)
plural = f'{flags.days} days' if flags.days != 1 else f'{flags.days} day'
await ctx.send(f'Banned {members} for {flags.reason!r} (deleted {plural} worth of messages)')
This is called by repeatedly specifying the flag:
.. image:: /images/commands/flags2.png
typing.Tuple
^^^^^^^^^^^^^
Since the above syntax can be a bit repetitive when specifying a flag many times, the :class:`py:tuple` type annotation
allows for "greedy-like" semantics using a variadic tuple:
.. code-block:: python3
from discord.ext import commands
from typing import Tuple
import discord
class BanFlags(commands.FlagConverter):
members: Tuple[discord.Member, ...]
reason: str
days: int = 1
This allows the previous ``ban`` command to be called like this:
.. image:: /images/commands/flags3.png
The :class:`py:tuple` annotation also allows for parsing of pairs. For example, given the following code:
.. code-block:: python3
# point: 10 11 point: 12 13
class Coordinates(commands.FlagConverter):
point: Tuple[int, int]
.. warning::
Due to potential parsing ambiguities, the parser expects tuple arguments to be quoted
if they require spaces. So if one of the inner types is :class:`str` and the argument requires spaces
then quotes should be used to disambiguate it from the other element of the tuple.
typing.Dict
^^^^^^^^^^^^^
A :class:`dict` annotation is functionally equivalent to ``List[Tuple[K, V]]`` except with the return type
given as a :class:`dict` rather than a :class:`list`.
.. _ext_commands_error_handler:
@@ -575,7 +764,7 @@ When our commands fail to parse we will, by default, receive a noisy error in ``
that an error has happened and has been silently ignored.
In order to handle our errors, we must use something called an error handler. There is a global error handler, called
:func:`on_command_error` which works like any other event in the :ref:`discord-api-events`. This global error handler is
:func:`.on_command_error` which works like any other event in the :ref:`discord-api-events`. This global error handler is
called for every error reached.
Most of the time however, we want to handle an error local to the command itself. Luckily, commands come with local error
@@ -586,8 +775,8 @@ handlers that allow us to do just that. First we decorate an error handler funct
@bot.command()
async def info(ctx, *, member: discord.Member):
"""Tells you some info about the member."""
fmt = '{0} joined on {0.joined_at} and has {1} roles.'
await ctx.send(fmt.format(member, len(member.roles)))
msg = f'{member} joined on {member.joined_at} and has {len(member.roles)} roles.'
await ctx.send(msg)
@info.error
async def info_error(ctx, error):

View File

@@ -22,12 +22,12 @@ An example extension looks like this:
@commands.command()
async def hello(ctx):
await ctx.send('Hello {0.display_name}.'.format(ctx.author))
await ctx.send(f'Hello {ctx.author.display_name}.')
def setup(bot):
bot.add_command(hello)
In this example we define a simple command, and when the extension is loaded this command is added to the bot. Now the final step to this is loading the extension, which we do by calling :meth:`.commands.Bot.load_extension`. To load this extension we call ``bot.load_extension('hello')``.
In this example we define a simple command, and when the extension is loaded this command is added to the bot. Now the final step to this is loading the extension, which we do by calling :meth:`.Bot.load_extension`. To load this extension we call ``bot.load_extension('hello')``.
.. admonition:: Cogs
:class: helpful
@@ -41,7 +41,7 @@ In this example we define a simple command, and when the extension is loaded thi
Reloading
-----------
When you make a change to the extension and want to reload the references, the library comes with a function to do this for you, :meth:`Bot.reload_extension`.
When you make a change to the extension and want to reload the references, the library comes with a function to do this for you, :meth:`.Bot.reload_extension`.
.. code-block:: python3

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