├── .devcontainer
└── devcontainer.json
├── .editorconfig
├── .github
├── actions
│ └── setup-python
│ │ └── action.yml
└── workflows
│ ├── release.yml
│ └── ruff.yml
├── .gitignore
├── .pre-commit-config.yaml
├── README.md
├── assets
└── logo.png
├── nonebot
└── adapters
│ └── discord
│ ├── __init__.py
│ ├── adapter.py
│ ├── api
│ ├── __init__.py
│ ├── client.py
│ ├── client.pyi
│ ├── handle.py
│ ├── model.py
│ ├── request.py
│ ├── types.py
│ └── utils.py
│ ├── bot.py
│ ├── commands
│ ├── __init__.py
│ ├── matcher.py
│ ├── params.py
│ └── storage.py
│ ├── config.py
│ ├── event.py
│ ├── exception.py
│ ├── message.py
│ ├── payload.py
│ └── utils.py
├── poetry.lock
└── pyproject.toml
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Default Linux Universal",
3 | "image": "mcr.microsoft.com/devcontainers/universal:2-linux",
4 | "features": {
5 | "ghcr.io/devcontainers-contrib/features/poetry:1": {}
6 | },
7 | "postCreateCommand": "poetry config virtualenvs.in-project true && poetry install && poetry run pre-commit install",
8 | "customizations": {
9 | "vscode": {
10 | "settings": {
11 | "python.analysis.diagnosticMode": "workspace",
12 | "python.analysis.typeCheckingMode": "basic",
13 | "[python]": {
14 | "editor.defaultFormatter": "ms-python.black-formatter",
15 | "editor.codeActionsOnSave": {
16 | "source.organizeImports": true
17 | }
18 | },
19 | "[javascript]": {
20 | "editor.defaultFormatter": "esbenp.prettier-vscode"
21 | },
22 | "[html]": {
23 | "editor.defaultFormatter": "esbenp.prettier-vscode"
24 | },
25 | "[typescript]": {
26 | "editor.defaultFormatter": "esbenp.prettier-vscode"
27 | },
28 | "[javascriptreact]": {
29 | "editor.defaultFormatter": "esbenp.prettier-vscode"
30 | },
31 | "[typescriptreact]": {
32 | "editor.defaultFormatter": "esbenp.prettier-vscode"
33 | },
34 | "files.exclude": {
35 | "**/__pycache__": true
36 | },
37 | "files.watcherExclude": {
38 | "**/target/**": true,
39 | "**/__pycache__": true
40 | }
41 | },
42 | "extensions": [
43 | "ms-python.python",
44 | "ms-python.vscode-pylance",
45 | "ms-python.isort",
46 | "ms-python.black-formatter",
47 | "EditorConfig.EditorConfig",
48 | "esbenp.prettier-vscode"
49 | ]
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | # The JSON files contain newlines inconsistently
13 | [*.json]
14 | insert_final_newline = ignore
15 |
16 | # Minified JavaScript files shouldn't be changed
17 | [**.min.js]
18 | indent_style = ignore
19 | insert_final_newline = ignore
20 |
21 | # Makefiles always use tabs for indentation
22 | [Makefile]
23 | indent_style = tab
24 |
25 | # Batch files use tabs for indentation
26 | [*.bat]
27 | indent_style = tab
28 |
29 | [*.md]
30 | trim_trailing_whitespace = false
31 |
32 | # Matches the exact files either package.json or .travis.yml
33 | [{package.json,.travis.yml}]
34 | indent_size = 2
35 |
36 | [{*.py,*.pyi}]
37 | indent_size = 4
38 |
--------------------------------------------------------------------------------
/.github/actions/setup-python/action.yml:
--------------------------------------------------------------------------------
1 | name: Setup Python
2 | description: Setup Python
3 |
4 | inputs:
5 | python-version:
6 | description: Python version
7 | required: false
8 | default: "3.10"
9 |
10 | runs:
11 | using: "composite"
12 | steps:
13 | - name: Install poetry
14 | run: pipx install poetry
15 | shell: bash
16 |
17 | - uses: actions/setup-python@v4
18 | with:
19 | python-version: ${{ inputs.python-version }}
20 | architecture: "x64"
21 | cache: "poetry"
22 |
23 | - run: poetry install
24 | shell: bash
25 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | id-token: write
13 | contents: write
14 | steps:
15 | - uses: actions/checkout@v3
16 |
17 | - name: Setup Python environment
18 | uses: ./.github/actions/setup-python
19 |
20 | - name: Get Version
21 | id: version
22 | run: |
23 | echo "VERSION=$(poetry version -s)" >> $GITHUB_OUTPUT
24 | echo "TAG_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
25 | echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
26 |
27 | - name: Check Version
28 | if: steps.version.outputs.VERSION != steps.version.outputs.TAG_VERSION
29 | run: exit 1
30 |
31 | - name: Build Package
32 | run: poetry build
33 |
34 | - name: Publish Package to PyPI
35 | uses: pypa/gh-action-pypi-publish@release/v1
36 |
37 | - name: Publish Package to GitHub
38 | run: |
39 | gh release upload --clobber ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl
40 | env:
41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42 |
--------------------------------------------------------------------------------
/.github/workflows/ruff.yml:
--------------------------------------------------------------------------------
1 | name: Ruff Lint
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | jobs:
10 | ruff:
11 | name: Ruff Lint
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - name: Run Ruff Lint
17 | uses: chartboost/ruff-action@v1
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # ----- Project -----
2 |
3 | # Created by https://www.toptal.com/developers/gitignore/api/python,node,visualstudiocode,jetbrains,macos,windows,linux
4 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,node,visualstudiocode,jetbrains,macos,windows,linux
5 |
6 | ### JetBrains ###
7 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
8 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
9 |
10 | # User-specific stuff
11 | .idea/**/workspace.xml
12 | .idea/**/tasks.xml
13 | .idea/**/usage.statistics.xml
14 | .idea/**/dictionaries
15 | .idea/**/shelf
16 |
17 | # AWS User-specific
18 | .idea/**/aws.xml
19 |
20 | # Generated files
21 | .idea/**/contentModel.xml
22 |
23 | # Sensitive or high-churn files
24 | .idea/**/dataSources/
25 | .idea/**/dataSources.ids
26 | .idea/**/dataSources.local.xml
27 | .idea/**/sqlDataSources.xml
28 | .idea/**/dynamic.xml
29 | .idea/**/uiDesigner.xml
30 | .idea/**/dbnavigator.xml
31 |
32 | # Gradle
33 | .idea/**/gradle.xml
34 | .idea/**/libraries
35 |
36 | # Gradle and Maven with auto-import
37 | # When using Gradle or Maven with auto-import, you should exclude module files,
38 | # since they will be recreated, and may cause churn. Uncomment if using
39 | # auto-import.
40 | # .idea/artifacts
41 | # .idea/compiler.xml
42 | # .idea/jarRepositories.xml
43 | # .idea/modules.xml
44 | # .idea/*.iml
45 | # .idea/modules
46 | # *.iml
47 | # *.ipr
48 |
49 | # CMake
50 | cmake-build-*/
51 |
52 | # Mongo Explorer plugin
53 | .idea/**/mongoSettings.xml
54 |
55 | # File-based project format
56 | *.iws
57 |
58 | # IntelliJ
59 | out/
60 |
61 | # mpeltonen/sbt-idea plugin
62 | .idea_modules/
63 |
64 | # JIRA plugin
65 | atlassian-ide-plugin.xml
66 |
67 | # Cursive Clojure plugin
68 | .idea/replstate.xml
69 |
70 | # SonarLint plugin
71 | .idea/sonarlint/
72 |
73 | # Crashlytics plugin (for Android Studio and IntelliJ)
74 | com_crashlytics_export_strings.xml
75 | crashlytics.properties
76 | crashlytics-build.properties
77 | fabric.properties
78 |
79 | # Editor-based Rest Client
80 | .idea/httpRequests
81 |
82 | # Android studio 3.1+ serialized cache file
83 | .idea/caches/build_file_checksums.ser
84 |
85 | ### JetBrains Patch ###
86 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
87 |
88 | # *.iml
89 | # modules.xml
90 | # .idea/misc.xml
91 | # *.ipr
92 |
93 | # Sonarlint plugin
94 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
95 | .idea/**/sonarlint/
96 |
97 | # SonarQube Plugin
98 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
99 | .idea/**/sonarIssues.xml
100 |
101 | # Markdown Navigator plugin
102 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
103 | .idea/**/markdown-navigator.xml
104 | .idea/**/markdown-navigator-enh.xml
105 | .idea/**/markdown-navigator/
106 |
107 | # Cache file creation bug
108 | # See https://youtrack.jetbrains.com/issue/JBR-2257
109 | .idea/$CACHE_FILE$
110 |
111 | # CodeStream plugin
112 | # https://plugins.jetbrains.com/plugin/12206-codestream
113 | .idea/codestream.xml
114 |
115 | # Azure Toolkit for IntelliJ plugin
116 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
117 | .idea/**/azureSettings.xml
118 |
119 | ### Linux ###
120 | *~
121 |
122 | # temporary files which can be created if a process still has a handle open of a deleted file
123 | .fuse_hidden*
124 |
125 | # KDE directory preferences
126 | .directory
127 |
128 | # Linux trash folder which might appear on any partition or disk
129 | .Trash-*
130 |
131 | # .nfs files are created when an open file is removed but is still being accessed
132 | .nfs*
133 |
134 | ### macOS ###
135 | # General
136 | .DS_Store
137 | .AppleDouble
138 | .LSOverride
139 |
140 | # Icon must end with two \r
141 | Icon
142 |
143 |
144 | # Thumbnails
145 | ._*
146 |
147 | # Files that might appear in the root of a volume
148 | .DocumentRevisions-V100
149 | .fseventsd
150 | .Spotlight-V100
151 | .TemporaryItems
152 | .Trashes
153 | .VolumeIcon.icns
154 | .com.apple.timemachine.donotpresent
155 |
156 | # Directories potentially created on remote AFP share
157 | .AppleDB
158 | .AppleDesktop
159 | Network Trash Folder
160 | Temporary Items
161 | .apdisk
162 |
163 | ### macOS Patch ###
164 | # iCloud generated files
165 | *.icloud
166 |
167 | ### Node ###
168 | # Logs
169 | logs
170 | *.log
171 | npm-debug.log*
172 | yarn-debug.log*
173 | yarn-error.log*
174 | lerna-debug.log*
175 | .pnpm-debug.log*
176 |
177 | # Diagnostic reports (https://nodejs.org/api/report.html)
178 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
179 |
180 | # Runtime data
181 | pids
182 | *.pid
183 | *.seed
184 | *.pid.lock
185 |
186 | # Directory for instrumented libs generated by jscoverage/JSCover
187 | lib-cov
188 |
189 | # Coverage directory used by tools like istanbul
190 | coverage
191 | *.lcov
192 |
193 | # nyc test coverage
194 | .nyc_output
195 |
196 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
197 | .grunt
198 |
199 | # Bower dependency directory (https://bower.io/)
200 | bower_components
201 |
202 | # node-waf configuration
203 | .lock-wscript
204 |
205 | # Compiled binary addons (https://nodejs.org/api/addons.html)
206 | build/Release
207 |
208 | # Dependency directories
209 | node_modules/
210 | jspm_packages/
211 |
212 | # Snowpack dependency directory (https://snowpack.dev/)
213 | web_modules/
214 |
215 | # TypeScript cache
216 | *.tsbuildinfo
217 |
218 | # Optional npm cache directory
219 | .npm
220 |
221 | # Optional eslint cache
222 | .eslintcache
223 |
224 | # Optional stylelint cache
225 | .stylelintcache
226 |
227 | # Microbundle cache
228 | .rpt2_cache/
229 | .rts2_cache_cjs/
230 | .rts2_cache_es/
231 | .rts2_cache_umd/
232 |
233 | # Optional REPL history
234 | .node_repl_history
235 |
236 | # Output of 'npm pack'
237 | *.tgz
238 |
239 | # Yarn Integrity file
240 | .yarn-integrity
241 |
242 | # dotenv environment variable files
243 | .env
244 | .env.development.local
245 | .env.test.local
246 | .env.production.local
247 | .env.local
248 |
249 | # parcel-bundler cache (https://parceljs.org/)
250 | .cache
251 | .parcel-cache
252 |
253 | # Next.js build output
254 | .next
255 | out
256 |
257 | # Nuxt.js build / generate output
258 | .nuxt
259 | dist
260 |
261 | # Gatsby files
262 | .cache/
263 | # Comment in the public line in if your project uses Gatsby and not Next.js
264 | # https://nextjs.org/blog/next-9-1#public-directory-support
265 | # public
266 |
267 | # vuepress build output
268 | .vuepress/dist
269 |
270 | # vuepress v2.x temp and cache directory
271 | .temp
272 |
273 | # Docusaurus cache and generated files
274 | .docusaurus
275 |
276 | # Serverless directories
277 | .serverless/
278 |
279 | # FuseBox cache
280 | .fusebox/
281 |
282 | # DynamoDB Local files
283 | .dynamodb/
284 |
285 | # TernJS port file
286 | .tern-port
287 |
288 | # Stores VSCode versions used for testing VSCode extensions
289 | .vscode-test
290 |
291 | # yarn v2
292 | .yarn/cache
293 | .yarn/unplugged
294 | .yarn/build-state.yml
295 | .yarn/install-state.gz
296 | .pnp.*
297 |
298 | ### Node Patch ###
299 | # Serverless Webpack directories
300 | .webpack/
301 |
302 | # Optional stylelint cache
303 |
304 | # SvelteKit build / generate output
305 | .svelte-kit
306 |
307 | ### Python ###
308 | # Byte-compiled / optimized / DLL files
309 | __pycache__/
310 | *.py[cod]
311 | *$py.class
312 |
313 | # C extensions
314 | *.so
315 |
316 | # Distribution / packaging
317 | .Python
318 | build/
319 | develop-eggs/
320 | dist/
321 | downloads/
322 | eggs/
323 | .eggs/
324 | lib/
325 | lib64/
326 | parts/
327 | sdist/
328 | var/
329 | wheels/
330 | share/python-wheels/
331 | *.egg-info/
332 | .installed.cfg
333 | *.egg
334 | MANIFEST
335 |
336 | # PyInstaller
337 | # Usually these files are written by a python script from a template
338 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
339 | *.manifest
340 | *.spec
341 |
342 | # Installer logs
343 | pip-log.txt
344 | pip-delete-this-directory.txt
345 |
346 | # Unit test / coverage reports
347 | htmlcov/
348 | .tox/
349 | .nox/
350 | .coverage
351 | .coverage.*
352 | nosetests.xml
353 | coverage.xml
354 | *.cover
355 | *.py,cover
356 | .hypothesis/
357 | .pytest_cache/
358 | cover/
359 |
360 | # Translations
361 | *.mo
362 | *.pot
363 |
364 | # Django stuff:
365 | local_settings.py
366 | db.sqlite3
367 | db.sqlite3-journal
368 |
369 | # Flask stuff:
370 | instance/
371 | .webassets-cache
372 |
373 | # Scrapy stuff:
374 | .scrapy
375 |
376 | # Sphinx documentation
377 | docs/_build/
378 |
379 | # PyBuilder
380 | .pybuilder/
381 | target/
382 |
383 | # Jupyter Notebook
384 | .ipynb_checkpoints
385 |
386 | # IPython
387 | profile_default/
388 | ipython_config.py
389 |
390 | # pyenv
391 | # For a library or package, you might want to ignore these files since the code is
392 | # intended to run in multiple environments; otherwise, check them in:
393 | # .python-version
394 |
395 | # pipenv
396 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
397 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
398 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
399 | # install all needed dependencies.
400 | #Pipfile.lock
401 |
402 | # poetry
403 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
404 | # This is especially recommended for binary packages to ensure reproducibility, and is more
405 | # commonly ignored for libraries.
406 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
407 | #poetry.lock
408 |
409 | # pdm
410 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
411 | #pdm.lock
412 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
413 | # in version control.
414 | # https://pdm.fming.dev/#use-with-ide
415 | .pdm.toml
416 |
417 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
418 | __pypackages__/
419 |
420 | # Celery stuff
421 | celerybeat-schedule
422 | celerybeat.pid
423 |
424 | # SageMath parsed files
425 | *.sage.py
426 |
427 | # Environments
428 | .venv
429 | env/
430 | venv/
431 | ENV/
432 | env.bak/
433 | venv.bak/
434 |
435 | # Spyder project settings
436 | .spyderproject
437 | .spyproject
438 |
439 | # Rope project settings
440 | .ropeproject
441 |
442 | # mkdocs documentation
443 | /site
444 |
445 | # mypy
446 | .mypy_cache/
447 | .dmypy.json
448 | dmypy.json
449 |
450 | # Pyre type checker
451 | .pyre/
452 |
453 | # pytype static type analyzer
454 | .pytype/
455 |
456 | # Cython debug symbols
457 | cython_debug/
458 |
459 | # PyCharm
460 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
461 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
462 | # and can be added to the global gitignore or merged into this file. For a more nuclear
463 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
464 | #.idea/
465 |
466 | ### Python Patch ###
467 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
468 | poetry.toml
469 |
470 |
471 | ### VisualStudioCode ###
472 | .vscode/*
473 | !.vscode/settings.json
474 | !.vscode/tasks.json
475 | !.vscode/launch.json
476 | !.vscode/extensions.json
477 | !.vscode/*.code-snippets
478 |
479 | # Local History for Visual Studio Code
480 | .history/
481 |
482 | # Built Visual Studio Code Extensions
483 | *.vsix
484 |
485 | ### VisualStudioCode Patch ###
486 | # Ignore all local history of files
487 | .history
488 | .ionide
489 |
490 | ### Windows ###
491 | # Windows thumbnail cache files
492 | Thumbs.db
493 | Thumbs.db:encryptable
494 | ehthumbs.db
495 | ehthumbs_vista.db
496 |
497 | # Dump file
498 | *.stackdump
499 |
500 | # Folder config file
501 | [Dd]esktop.ini
502 |
503 | # Recycle Bin used on file shares
504 | $RECYCLE.BIN/
505 |
506 | # Windows Installer files
507 | *.cab
508 | *.msi
509 | *.msix
510 | *.msm
511 | *.msp
512 |
513 | # Windows shortcuts
514 | *.lnk
515 |
516 | # End of https://www.toptal.com/developers/gitignore/api/python,node,visualstudiocode,jetbrains,macos,windows,linux
517 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | default_install_hook_types: [pre-commit, prepare-commit-msg]
2 | ci:
3 | autofix_commit_msg: ":rotating_light: auto fix by pre-commit hooks"
4 | autofix_prs: true
5 | autoupdate_branch: master
6 | autoupdate_schedule: monthly
7 | autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
8 | repos:
9 | - repo: https://github.com/astral-sh/ruff-pre-commit
10 | rev: v0.4.7
11 | hooks:
12 | - id: ruff
13 | args: [--fix, --exit-non-zero-on-fix]
14 | stages: [commit]
15 | - id: ruff-format
16 |
17 | - repo: https://github.com/pre-commit/pre-commit-hooks
18 | rev: v4.6.0
19 | hooks:
20 | - id: end-of-file-fixer
21 | - id: trailing-whitespace
22 |
23 | - repo: https://github.com/nonebot/nonemoji
24 | rev: v0.1.4
25 | hooks:
26 | - id: nonemoji
27 | stages: [prepare-commit-msg]
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # NoneBot-Adapter-Discord
8 |
9 | _✨ Discord 协议适配 ✨_
10 |
11 |
12 |
13 | ## 安装
14 |
15 | 通过 `nb adapter install nonebot-adapter-discord` 安装本适配器。
16 |
17 | 或在 `nb create` 创建项目时选择 `Discord` 适配器。
18 |
19 | 可通过 `pip install git+https://github.com/nonebot/adapter-discord.git@master` 安装开发中版本。
20 |
21 | 由于 [Discord 文档](https://discord.com/developers/docs/intro)存在部分表述不清的地方,并且结构复杂,存在很多 `partial object`,
22 | 需要更多实际测试以找出问题,欢迎提出 ISSUE 反馈。
23 |
24 | ## 配置
25 |
26 | 修改 NoneBot 配置文件 `.env` 或者 `.env.*`。
27 |
28 | ### Driver
29 |
30 | 参考 [driver](https://v2.nonebot.dev/docs/tutorial/configuration#driver) 配置项,添加 `ForwardDriver` 支持。
31 |
32 | 如:
33 |
34 | ```dotenv
35 | DRIVER=~httpx+~websockets
36 | ```
37 |
38 | ### DISCORD_BOTS
39 |
40 | 配置机器人帐号,如:
41 |
42 | ```dotenv
43 | DISCORD_BOTS='
44 | [
45 | {
46 | "token": "xxx",
47 | "intent": {
48 | "guild_messages": true,
49 | "direct_messages": true
50 | },
51 | "application_commands": {"*": ["*"]}
52 | }
53 | ]
54 | '
55 |
56 | # application_commands的{"*": ["*"]}代表将全部应用命令注册为全局应用命令
57 | # {"admin": ["123", "456"]}则代表将admin命令注册为id是123、456服务器的局部命令,其余命令不注册
58 | ```
59 |
60 | ### DISCORD_COMPRESS
61 |
62 | 是否启用数据压缩,默认为 `False`,如:
63 |
64 | ```dotenv
65 | DISCORD_COMPRESS=True
66 | ```
67 |
68 | ### DISCORD_API_VERSION
69 |
70 | Discord API 版本,默认为 `10`,如:
71 |
72 | ```dotenv
73 | DISCORD_API_VERSION=10
74 | ```
75 |
76 | ### DISCORD_API_TIMEOUT
77 |
78 | Discord API 超时时间,默认为 `30` 秒,如:
79 |
80 | ```dotenv
81 | DISCORD_API_TIMEOUT=15.0
82 | ```
83 |
84 | ### DISCORD_HANDLE_SELF_MESSAGE
85 |
86 | 是否处理自己发送的消息,默认为 `False`,如:
87 |
88 | ```dotenv
89 | DISCORD_HANDLE_SELF_MESSAGE=True
90 | ```
91 |
92 | ### DISCORD_PROXY
93 |
94 | 代理设置,默认无,如:
95 |
96 | ```dotenv
97 | DISCORD_PROXY='http://127.0.0.1:6666'
98 | ```
99 |
100 | ## 插件示例
101 |
102 | 以下是一个简单的插件示例,展示各种消息段:
103 |
104 | ```python
105 | import datetime
106 |
107 | from nonebot import on_command
108 | from nonebot.params import CommandArg
109 |
110 | from nonebot.adapters.discord import Bot, MessageEvent, MessageSegment, Message
111 | from nonebot.adapters.discord.api import *
112 |
113 | matcher = on_command('send')
114 |
115 |
116 | @matcher.handle()
117 | async def ready(bot: Bot, event: MessageEvent, msg: Message = CommandArg()):
118 | # 调用discord的api
119 | self_info = await bot.get_current_user() # 获取机器人自身信息
120 | user = await bot.get_user(user_id=event.user_id) # 获取指定用户信息
121 | ...
122 | # 各种消息段
123 | msg = msg.extract_plain_text()
124 | if msg == 'mention_me':
125 | # 发送一个提及你的消息
126 | await matcher.finish(MessageSegment.mention_user(event.user_id))
127 | elif msg == 'time':
128 | # 发送一个时间,使用相对时间(RelativeTime)样式
129 | await matcher.finish(MessageSegment.timestamp(datetime.datetime.now(),
130 | style=TimeStampStyle.RelativeTime))
131 | elif msg == 'mention_everyone':
132 | # 发送一个提及全体成员的消息
133 | await matcher.finish(MessageSegment.mention_everyone())
134 | elif msg == 'mention_channel':
135 | # 发送一个提及当前频道的消息
136 | await matcher.finish(MessageSegment.mention_channel(event.channel_id))
137 | elif msg == 'embed':
138 | # 发送一个嵌套消息,其中包含a embed标题,nonebot logo描述和来自网络url的logo图片
139 | await matcher.finish(MessageSegment.embed(
140 | Embed(title='a embed',
141 | type=EmbedTypes.image,
142 | description='nonebot logo',
143 | image=EmbedImage(
144 | url='https://v2.nonebot.dev/logo.png'))))
145 | elif msg == 'attachment':
146 | # 发送一个附件,其中包含来自本地的logo.png图片
147 | with open('logo.png', 'rb') as f:
148 | await matcher.finish(MessageSegment.attachment(file='logo.png',
149 | content=f.read()))
150 | elif msg == 'component':
151 | # 发送一个复杂消息,其中包含一个当前时间,一个字符串选择菜单,一个用户选择菜单和一个按钮
152 | time_now = MessageSegment.timestamp(datetime.datetime.now())
153 | string_select = MessageSegment.component(
154 | SelectMenu(type=ComponentType.StringSelect,
155 | custom_id='string select',
156 | placeholder='select a value',
157 | options=[
158 | SelectOption(label='A', value='a'),
159 | SelectOption(label='B', value='b'),
160 | SelectOption(label='C', value='c')]))
161 | select = MessageSegment.component(SelectMenu(
162 | type=ComponentType.UserInput,
163 | custom_id='user_input',
164 | placeholder='please select a user'))
165 | button = MessageSegment.component(
166 | Button(label='button',
167 | custom_id='button',
168 | style=ButtonStyle.Primary))
169 | await matcher.finish('now time:' + time_now + string_select + select + button)
170 | else:
171 | # 发送一个文本消息
172 | await matcher.finish(MessageSegment.text(msg))
173 | ```
174 |
175 | 以下是一个 Discord 斜杠命令的插件示例:
176 |
177 | ```python
178 | import asyncio
179 | from typing import Optional
180 |
181 | from nonebot.adapters.discord.api import (
182 | IntegerOption,
183 | NumberOption,
184 | StringOption,
185 | SubCommandOption,
186 | User,
187 | UserOption,
188 | )
189 | from nonebot.adapters.discord.commands import (
190 | CommandOption,
191 | on_slash_command,
192 | )
193 |
194 | matcher = on_slash_command(
195 | name="permission",
196 | description="权限管理",
197 | options=[
198 | SubCommandOption(
199 | name="add",
200 | description="添加",
201 | options=[
202 | StringOption(
203 | name="plugin",
204 | description="插件名",
205 | required=True,
206 | ),
207 | IntegerOption(
208 | name="priority",
209 | description="优先级",
210 | required=False,
211 | ),
212 | ],
213 | ),
214 | SubCommandOption(
215 | name="remove",
216 | description="移除",
217 | options=[
218 | StringOption(name="plugin", description="插件名", required=True),
219 | NumberOption(name="time", description="时长", required=False),
220 | ],
221 | ),
222 | SubCommandOption(
223 | name="ban",
224 | description="禁用",
225 | options=[
226 | UserOption(name="user", description="用户", required=False),
227 | ],
228 | ),
229 | ],
230 | )
231 |
232 |
233 | @matcher.handle_sub_command("add")
234 | async def handle_user_add(
235 | plugin: CommandOption[str], priority: CommandOption[Optional[int]]
236 | ):
237 | await matcher.send_deferred_response()
238 | await asyncio.sleep(2)
239 | await matcher.edit_response(f"你添加了插件 {plugin},优先级 {priority}")
240 | await asyncio.sleep(2)
241 | fm = await matcher.send_followup_msg(
242 | f"你添加了插件 {plugin},优先级 {priority} (新消息)"
243 | )
244 | await asyncio.sleep(2)
245 | await matcher.edit_followup_msg(
246 | fm.id, f"你添加了插件 {plugin},优先级 {priority} (新消息修改后)"
247 | )
248 |
249 |
250 | @matcher.handle_sub_command("remove")
251 | async def handle_user_remove(
252 | plugin: CommandOption[str], time: CommandOption[Optional[float]]
253 | ):
254 | await matcher.send(f"你移除了插件 {plugin},时长 {time}")
255 |
256 |
257 | @matcher.handle_sub_command("ban")
258 | async def handle_admin_ban(user: CommandOption[User]):
259 | await matcher.finish(f"你禁用了用户 {user.username}")
260 | ```
261 |
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonebot/adapter-discord/5794c0b53b4fe1f41726fd09b4db1a5239ef965b/assets/logo.png
--------------------------------------------------------------------------------
/nonebot/adapters/discord/__init__.py:
--------------------------------------------------------------------------------
1 | from .adapter import Adapter as Adapter
2 | from .bot import Bot as Bot
3 | from .commands import *
4 | from .event import *
5 | from .message import (
6 | Message as Message,
7 | MessageSegment as MessageSegment,
8 | )
9 | from .utils import log as log
10 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/adapter.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import contextlib
3 | import json
4 | import sys
5 | from typing import Any, Optional
6 | from typing_extensions import override
7 |
8 | from nonebot.adapters import Adapter as BaseAdapter
9 | from nonebot.compat import type_validate_json, type_validate_python
10 | from nonebot.drivers import URL, Driver, ForwardDriver, Request, WebSocket
11 | from nonebot.exception import WebSocketClosed
12 | from nonebot.plugin import get_plugin_config
13 | from nonebot.utils import escape_tag
14 |
15 | from .api.handle import API_HANDLERS
16 | from .api.model import GatewayBot, User
17 | from .bot import Bot
18 | from .commands import sync_application_command
19 | from .config import BotInfo, Config
20 | from .event import Event, MessageEvent, ReadyEvent, event_classes
21 | from .exception import ApiNotAvailable
22 | from .payload import (
23 | Dispatch,
24 | Heartbeat,
25 | HeartbeatAck,
26 | Hello,
27 | Identify,
28 | InvalidSession,
29 | Payload,
30 | PayloadType,
31 | Reconnect,
32 | Resume,
33 | )
34 | from .utils import decompress_data, log, model_dump
35 |
36 | RECONNECT_INTERVAL = 3.0
37 |
38 |
39 | class Adapter(BaseAdapter):
40 | @override
41 | def __init__(self, driver: Driver, **kwargs: Any):
42 | super().__init__(driver, **kwargs)
43 | self.discord_config: Config = get_plugin_config(Config)
44 | self.tasks: list[asyncio.Task] = []
45 | self.base_url: URL = URL(
46 | f"https://discord.com/api/v{self.discord_config.discord_api_version}",
47 | )
48 | self.setup()
49 |
50 | @classmethod
51 | @override
52 | def get_name(cls) -> str:
53 | return "Discord"
54 |
55 | def setup(self) -> None:
56 | if not isinstance(self.driver, ForwardDriver):
57 | raise RuntimeError(
58 | f"Current driver {self.config.driver} "
59 | "doesn't support forward connections!"
60 | "Discord Adapter need a ForwardDriver to work.",
61 | )
62 | self.on_ready(self.startup)
63 | self.driver.on_shutdown(self.shutdown)
64 | self.driver.on_bot_connect(sync_application_command)
65 |
66 | async def startup(self) -> None:
67 | log("INFO", "Discord Adapter is starting up...")
68 |
69 | log("DEBUG", f"Discord api base url: {escape_tag(str(self.base_url))}")
70 |
71 | for bot_info in self.discord_config.discord_bots:
72 | self.tasks.append(asyncio.create_task(self.run_bot(bot_info)))
73 |
74 | async def shutdown(self) -> None:
75 | for task in self.tasks:
76 | if not task.done():
77 | task.cancel()
78 |
79 | async def run_bot(self, bot_info: BotInfo) -> None:
80 | try:
81 | gateway_info = await self._get_gateway_bot(bot_info)
82 | if not gateway_info.url:
83 | raise ValueError("Failed to get gateway url")
84 | ws_url = URL(gateway_info.url)
85 | except Exception as e:
86 | log(
87 | "ERROR",
88 | "Failed to get gateway info.",
89 | e,
90 | )
91 | return
92 | remain = (
93 | gateway_info.session_start_limit
94 | and gateway_info.session_start_limit.remaining
95 | )
96 | if remain and remain <= 0:
97 | log(
98 | "ERROR",
99 | "Failed to establish connection to QQ Guild "
100 | "because of session start limit.\n"
101 | f"{escape_tag(str(gateway_info))}",
102 | )
103 | return
104 |
105 | if bot_info.shard is not None:
106 | self.tasks.append(
107 | asyncio.create_task(self._forward_ws(bot_info, ws_url, bot_info.shard)),
108 | )
109 | return
110 |
111 | shards = gateway_info.shards or 1
112 | for i in range(shards):
113 | self.tasks.append(
114 | asyncio.create_task(self._forward_ws(bot_info, ws_url, (i, shards))),
115 | )
116 | await asyncio.sleep(
117 | gateway_info.session_start_limit
118 | and gateway_info.session_start_limit.max_concurrency
119 | or 1,
120 | )
121 |
122 | async def _get_gateway_bot(self, bot_info: BotInfo) -> GatewayBot:
123 | headers = {"Authorization": self.get_authorization(bot_info)}
124 | request = Request(
125 | headers=headers,
126 | method="GET",
127 | url=self.base_url / "gateway/bot",
128 | timeout=self.discord_config.discord_api_timeout,
129 | proxy=self.discord_config.discord_proxy,
130 | )
131 | resp = await self.request(request)
132 | if not resp.content:
133 | raise ValueError("Failed to get gateway info")
134 | return type_validate_json(GatewayBot, resp.content)
135 |
136 | async def _get_bot_user(self, bot_info: BotInfo) -> User:
137 | headers = {"Authorization": self.get_authorization(bot_info)}
138 | request = Request(
139 | method="GET",
140 | url=self.base_url / "users/@me",
141 | headers=headers,
142 | timeout=self.discord_config.discord_api_timeout,
143 | proxy=self.discord_config.discord_proxy,
144 | )
145 | resp = await self.request(request)
146 | if not resp.content:
147 | raise ValueError("Failed to get bot user info")
148 | return type_validate_json(User, resp.content)
149 |
150 | async def _forward_ws(
151 | self,
152 | bot_info: BotInfo,
153 | ws_url: URL,
154 | shard: tuple[int, int],
155 | ) -> None:
156 | log("DEBUG", f"Forwarding WebSocket Connection to {escape_tag(str(ws_url))}...")
157 | headers = {"Authorization": self.get_authorization(bot_info)}
158 | params = {
159 | "v": self.discord_config.discord_api_version,
160 | "encoding": "json",
161 | }
162 | if self.discord_config.discord_compress:
163 | params["compress"] = "zlib-stream"
164 | request = Request(
165 | method="GET",
166 | url=ws_url,
167 | headers=headers,
168 | params=params,
169 | timeout=self.discord_config.discord_api_timeout,
170 | proxy=self.discord_config.discord_proxy,
171 | )
172 | heartbeat_task: Optional[asyncio.Task] = None
173 | bot: Optional[Bot] = None
174 | while True:
175 | try:
176 | if bot is None:
177 | user = await self._get_bot_user(bot_info)
178 | bot = Bot(self, str(user.id), bot_info)
179 | async with self.websocket(request) as ws:
180 | log(
181 | "DEBUG",
182 | "WebSocket Connection to"
183 | f" {escape_tag(str(ws_url))} established",
184 | )
185 | try:
186 | # 接收hello事件
187 | heartbeat_interval = await self._hello(ws)
188 | if not heartbeat_interval:
189 | await asyncio.sleep(RECONNECT_INTERVAL)
190 | continue
191 |
192 | if not heartbeat_task:
193 | # 发送第一次心跳
194 | log("DEBUG", "Waiting for first heartbeat to be send...")
195 | # await asyncio.sleep(
196 | # random.random() * heartbeat_interval / 1000.0) # https://discord.com/developers/docs/topics/gateway#heartbeat-interval
197 | await asyncio.sleep(5)
198 | await self._heartbeat(ws, bot)
199 | await self._heartbeat_ack(ws)
200 |
201 | # 开启心跳
202 | heartbeat_task = asyncio.create_task(
203 | self._heartbeat_task(ws, bot, heartbeat_interval),
204 | )
205 |
206 | # 进行identify和resume
207 | result = await self._authenticate(bot, ws, shard)
208 | if not result:
209 | await asyncio.sleep(RECONNECT_INTERVAL)
210 | continue
211 |
212 | # 处理事件
213 | await self._loop(bot, ws)
214 | except WebSocketClosed as e:
215 | log(
216 | "ERROR",
217 | "WebSocket Closed",
218 | e,
219 | )
220 | except Exception as e:
221 | log(
222 | "ERROR",
223 | "Error while process data from"
224 | f" websocket {escape_tag(str(ws_url))}. Trying to"
225 | " reconnect...",
226 | e,
227 | )
228 | finally:
229 | if heartbeat_task:
230 | heartbeat_task.cancel()
231 | heartbeat_task = None
232 | if bot.self_id in self.bots:
233 | self.bot_disconnect(bot)
234 |
235 | except Exception as e:
236 | log(
237 | "ERROR",
238 | "Error while setup websocket to "
239 | f"{escape_tag(str(ws_url))}. "
240 | "Trying to reconnect...",
241 | e,
242 | )
243 | await asyncio.sleep(RECONNECT_INTERVAL)
244 |
245 | async def _hello(self, ws: WebSocket):
246 | """接收并处理服务器的 Hello 事件
247 |
248 | 见 https://discord.com/developers/docs/topics/gateway#hello-event
249 | """
250 | try:
251 | payload = await self.receive_payload(ws)
252 | assert isinstance(
253 | payload,
254 | Hello,
255 | ), f"Received unexpected payload: {payload!r}"
256 | log("DEBUG", f"Received hello: {payload}")
257 | return payload.data.heartbeat_interval
258 | except Exception as e:
259 | log(
260 | "ERROR",
261 | "Error while receiving "
262 | "server hello event",
263 | e,
264 | )
265 |
266 | async def _heartbeat_ack(self, ws: WebSocket):
267 | """检查是否收到心跳ACK事件
268 |
269 | 见 https://discord.com/developers/docs/topics/gateway#sending-heartbeats"""
270 | payload = await self.receive_payload(ws)
271 | if not isinstance(payload, HeartbeatAck):
272 | await ws.close(1003) # 除了1000和1001都行
273 |
274 | @staticmethod
275 | async def _heartbeat(ws: WebSocket, bot: Bot):
276 | """心跳"""
277 | log("TRACE", f"Heartbeat {bot.sequence if bot.has_sequence else ''}")
278 | payload = type_validate_python(
279 | Heartbeat,
280 | {"data": bot.sequence if bot.has_sequence else None},
281 | )
282 | with contextlib.suppress(Exception):
283 | await ws.send(json.dumps(model_dump(payload, by_alias=True)))
284 |
285 | async def _heartbeat_task(self, ws: WebSocket, bot: Bot, heartbeat_interval: int):
286 | """心跳任务"""
287 | while True:
288 | await self._heartbeat(ws, bot)
289 | await asyncio.sleep(heartbeat_interval / 1000.0)
290 |
291 | async def _authenticate(self, bot: Bot, ws: WebSocket, shard: tuple[int, int]):
292 | """鉴权连接"""
293 | if not bot.ready:
294 | payload = type_validate_python(
295 | Identify,
296 | {
297 | "data": {
298 | "token": self.get_authorization(bot.bot_info),
299 | "intents": bot.bot_info.intent.to_int(),
300 | "shard": list(shard),
301 | "compress": self.discord_config.discord_compress,
302 | "properties": {
303 | "os": sys.platform,
304 | "browser": "NoneBot2",
305 | "device": "NoneBot2",
306 | },
307 | },
308 | },
309 | )
310 | else:
311 | payload = type_validate_python(
312 | Resume,
313 | {
314 | "data": {
315 | "token": self.get_authorization(bot.bot_info),
316 | "session_id": bot.session_id,
317 | "seq": bot.sequence,
318 | },
319 | },
320 | )
321 |
322 | try:
323 | await ws.send(
324 | json.dumps(model_dump(payload, by_alias=True, exclude_none=True))
325 | )
326 | except Exception as e:
327 | log(
328 | "ERROR",
329 | "Error while sending "
330 | + ("Identify" if isinstance(payload, Identify) else "Resume")
331 | + " event",
332 | e,
333 | )
334 | return None
335 |
336 | ready_event = None
337 | if not bot.ready:
338 | # https://discord.com/developers/docs/topics/gateway#ready-event
339 | # 鉴权成功之后,后台会下发一个 Ready Event
340 | payload = await self.receive_payload(ws)
341 | if isinstance(payload, HeartbeatAck):
342 | log(
343 | "WARNING", "Received unexpected HeartbeatAck event when identifying"
344 | )
345 | payload = await self.receive_payload(ws)
346 | assert isinstance(
347 | payload,
348 | Dispatch,
349 | ), f"Received unexpected payload: {payload!r}"
350 | bot.sequence = payload.sequence
351 | ready_event = self.payload_to_event(payload)
352 | assert isinstance(
353 | ready_event,
354 | ReadyEvent,
355 | ), f"Received unexpected event: {ready_event!r}"
356 | ws.request.url = URL(ready_event.resume_gateway_url)
357 | bot.session_id = ready_event.session_id
358 | bot.self_info = ready_event.user
359 |
360 | # only connect for single shard
361 | if bot.self_id not in self.bots:
362 | self.bot_connect(bot)
363 | log(
364 | "INFO",
365 | f"Bot {escape_tag(bot.self_id)} connected",
366 | )
367 |
368 | if ready_event:
369 | asyncio.create_task(bot.handle_event(ready_event))
370 |
371 | return True
372 |
373 | async def _loop(self, bot: Bot, ws: WebSocket):
374 | """接收并处理事件"""
375 | while True:
376 | payload = await self.receive_payload(ws)
377 | log(
378 | "TRACE",
379 | f"Received payload: {escape_tag(repr(payload))}",
380 | )
381 | if isinstance(payload, Dispatch):
382 | bot.sequence = payload.sequence
383 | try:
384 | event = self.payload_to_event(payload)
385 | except Exception as e:
386 | # (Path() / f"{payload.type}-{payload.sequence}.json").write_text(
387 | # json.dumps(model_dump(payload), indent=4, ensure_ascii=False),
388 | # encoding="utf-8",
389 | # )
390 | log(
391 | "WARNING",
392 | f"Failed to parse event {escape_tag(repr(payload))}",
393 | e,
394 | )
395 | else:
396 | if not (
397 | isinstance(event, MessageEvent)
398 | and event.get_user_id() == bot.self_id
399 | and not self.discord_config.discord_handle_self_message
400 | ):
401 | asyncio.create_task(bot.handle_event(event))
402 | elif isinstance(payload, Heartbeat):
403 | # 当接受到心跳payload时,需要立即发送一次心跳,见 https://discord.com/developers/docs/topics/gateway#heartbeat-requests
404 | await self._heartbeat(ws, bot)
405 |
406 | elif isinstance(payload, HeartbeatAck):
407 | log("TRACE", "Heartbeat ACK")
408 | continue
409 | elif isinstance(payload, Reconnect):
410 | log(
411 | "WARNING",
412 | "Received reconnect event from server. Try to reconnect...",
413 | )
414 | break
415 | elif isinstance(payload, InvalidSession):
416 | bot.clear()
417 | log(
418 | "ERROR",
419 | "Received invalid session event from server. Try to reconnect...",
420 | )
421 | break
422 | else:
423 | log(
424 | "WARNING",
425 | f"Unknown payload from server: {escape_tag(repr(payload))}",
426 | )
427 |
428 | @staticmethod
429 | def get_authorization(bot_info: BotInfo) -> str:
430 | return f"Bot {bot_info.token}"
431 |
432 | async def receive_payload(self, ws: WebSocket) -> Payload:
433 | data = await ws.receive()
434 | data = decompress_data(data, self.discord_config.discord_compress)
435 | return type_validate_json(PayloadType, data) # type: ignore
436 |
437 | @classmethod
438 | def payload_to_event(cls, payload: Dispatch) -> Event:
439 | EventClass = event_classes.get(payload.type, None)
440 | if not EventClass:
441 | log(
442 | "WARNING",
443 | f"Unknown payload type: {payload.type}, detail: {repr(payload)}",
444 | )
445 | event = type_validate_python(Event, payload.data)
446 | event.__type__ = payload.type # type: ignore
447 | return event
448 | return type_validate_python(EventClass, payload.data)
449 |
450 | @override
451 | async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:
452 | log("DEBUG", f"Calling API {api}")
453 | if (api_handler := API_HANDLERS.get(api)) is None:
454 | raise ApiNotAvailable
455 | return await api_handler(self, bot, **data)
456 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/api/__init__.py:
--------------------------------------------------------------------------------
1 | from .client import ApiClient as ApiClient
2 | from .handle import API_HANDLERS as API_HANDLERS
3 | from .model import *
4 | from .types import *
5 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/api/client.py:
--------------------------------------------------------------------------------
1 | class ApiClient: ...
2 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/api/request.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import TYPE_CHECKING, Any
3 |
4 | from nonebot.drivers import Request
5 | from nonebot.utils import escape_tag
6 |
7 | from ..exception import (
8 | ActionFailed,
9 | DiscordAdapterException,
10 | NetworkError,
11 | RateLimitException,
12 | UnauthorizedException,
13 | )
14 | from ..utils import decompress_data, log
15 |
16 | if TYPE_CHECKING:
17 | from ..adapter import Adapter
18 | from ..bot import Bot
19 |
20 |
21 | async def _request(adapter: "Adapter", bot: "Bot", request: Request) -> Any:
22 | try:
23 | request.timeout = adapter.discord_config.discord_api_timeout
24 | request.proxy = adapter.discord_config.discord_proxy
25 | data = await adapter.request(request)
26 | log(
27 | "TRACE",
28 | f"API code: {data.status_code} response: {escape_tag(str(data.content))}",
29 | )
30 | if data.status_code in (200, 201, 204):
31 | return data.content and json.loads(
32 | decompress_data(data.content, adapter.discord_config.discord_compress)
33 | )
34 | elif data.status_code in (401, 403):
35 | raise UnauthorizedException(data)
36 | elif data.status_code == 429:
37 | raise RateLimitException(data)
38 | else:
39 | raise ActionFailed(data)
40 | except DiscordAdapterException:
41 | raise
42 | except Exception as e:
43 | raise NetworkError("API request failed") from e
44 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/api/types.py:
--------------------------------------------------------------------------------
1 | from enum import Enum, IntEnum, IntFlag
2 | from typing import Any, Literal, TypeVar, Union, final
3 | from typing_extensions import TypeAlias
4 |
5 | from nonebot.compat import PYDANTIC_V2
6 |
7 | if PYDANTIC_V2:
8 | from pydantic_core import core_schema
9 |
10 | T = TypeVar("T")
11 |
12 |
13 | @final
14 | class Unset(Enum):
15 | _UNSET = ""
16 |
17 | def __repr__(self) -> str:
18 | return ""
19 |
20 | def __str__(self) -> str:
21 | return self.__repr__()
22 |
23 | def __bool__(self) -> Literal[False]:
24 | return False
25 |
26 | def __copy__(self):
27 | return self._UNSET
28 |
29 | def __deepcopy__(self, memo: dict[int, Any]):
30 | return self._UNSET
31 |
32 | if PYDANTIC_V2:
33 |
34 | @classmethod
35 | def __get_pydantic_core_schema__(
36 | cls, source_type: Any, handler: Any
37 | ) -> core_schema.CoreSchema:
38 | return core_schema.with_info_plain_validator_function(
39 | lambda value, _: cls._validate(value)
40 | )
41 | else:
42 |
43 | @classmethod
44 | def __get_validators__(cls):
45 | yield cls._validate
46 |
47 | @classmethod
48 | def _validate(cls, value: Any):
49 | if value is not cls._UNSET:
50 | raise ValueError(f"{value!r} is not UNSET")
51 | return value
52 |
53 |
54 | UNSET = Unset._UNSET
55 | """UNSET means that the field maybe not given in the data.
56 |
57 | see https://discord.com/developers/docs/reference#nullable-and-optional-resource-fields"""
58 |
59 | Missing: TypeAlias = Union[Literal[UNSET], T]
60 | """Missing means that the field maybe not given in the data.
61 |
62 | Missing[T] equal to Union[UNSET, T].
63 |
64 | example: Missing[int] == Union[UNSET, int]"""
65 |
66 | MissingOrNullable: TypeAlias = Union[Literal[UNSET], T, None]
67 | """MissingOrNullable means that the field maybe not given in the data or value is None.
68 |
69 | MissingOrNullable[T] equal to Union[UNSET, T, None].
70 |
71 | example: MissingOrNullable[int] == Union[UNSET, int, None]"""
72 |
73 |
74 | class StrEnum(str, Enum):
75 | """String enum."""
76 |
77 |
78 | class ActivityAssetImage(StrEnum):
79 | """Activity Asset Image
80 |
81 | see https://discord.com/developers/docs/topics/gateway-events#activity-object-activity-asset-image
82 | """
83 |
84 | ApplicationAsset = "Application Asset"
85 | """{application_asset_id} see https://discord.com/developers/docs/reference#image-formatting"""
86 | MediaProxyImage = "Media Proxy Image"
87 | """mp:{image_id}"""
88 |
89 |
90 | class ActivityFlags(IntFlag):
91 | """Activity Flags
92 |
93 | see https://discord.com/developers/docs/topics/gateway-events#activity-object-activity-flags
94 | """
95 |
96 | INSTANCE = 1 << 0
97 | JOIN = 1 << 1
98 | SPECTATE = 1 << 2
99 | JOIN_REQUEST = 1 << 3
100 | SYNC = 1 << 4
101 | PLAY = 1 << 5
102 | PARTY_PRIVACY_FRIENDS = 1 << 6
103 | PARTY_PRIVACY_VOICE_CHANNEL = 1 << 7
104 | EMBEDDED = 1 << 8
105 |
106 |
107 | class ActivityType(IntEnum):
108 | """Activity Type
109 |
110 | see https://discord.com/developers/docs/topics/gateway-events#activity-object-activity-types
111 | """
112 |
113 | Game = 0
114 | """Playing {name}"""
115 | Streaming = 1
116 | """Streaming {details}"""
117 | Listening = 2
118 | """Listening to {name}"""
119 | Watching = 3
120 | """Watching {name}"""
121 | Custom = 4
122 | """{emoji} {name}"""
123 | Competing = 5
124 | """ Competing in {name}"""
125 |
126 |
127 | class ApplicationCommandOptionType(IntEnum):
128 | """Application Command Option Type
129 |
130 | see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type
131 | """
132 |
133 | SUB_COMMAND = 1
134 | SUB_COMMAND_GROUP = 2
135 | STRING = 3
136 | INTEGER = 4
137 | """Any integer between -2^53 and 2^53"""
138 | BOOLEAN = 5
139 | USER = 6
140 | CHANNEL = 7
141 | """Includes all channel types + categories"""
142 | ROLE = 8
143 | MENTIONABLE = 9
144 | """Includes users and roles"""
145 | NUMBER = 10
146 | """Any double between -2^53 and 2^53"""
147 | ATTACHMENT = 11
148 | """attachment object"""
149 |
150 |
151 | class ApplicationCommandPermissionsType(IntEnum):
152 | """Application command permissions type.
153 |
154 | see https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permission-type
155 | """
156 |
157 | ROLE = 1
158 | USER = 2
159 | CHANNEL = 3
160 |
161 |
162 | class ApplicationCommandType(IntEnum):
163 | """Application Command Type
164 |
165 | see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-types
166 | """
167 |
168 | CHAT_INPUT = 1
169 | """Slash commands; a text-based command that shows up when a user types /"""
170 | USER = 2
171 | """A UI-based command that shows up when you right click or tap on a user"""
172 | MESSAGE = 3
173 | """A UI-based command that shows up when you right click or tap on a message"""
174 |
175 |
176 | class ApplicationFlag(IntFlag):
177 | """Application flags.
178 |
179 | see https://discord.com/developers/docs/resources/application#application-object-application-flags
180 | """
181 |
182 | APPLICATION_AUTO_MODERATION_RULE_CREATE_BADGE = 1 << 6
183 | """Indicates if an app uses the Auto Moderation API"""
184 | GATEWAY_PRESENCE = 1 << 12
185 | """Intent required for bots in 100 or more servers
186 | to receive presence_update events"""
187 | GATEWAY_PRESENCE_LIMITED = 1 << 13
188 | """Intent required for bots in under 100 servers to receive presence_update events,
189 | found on the Bot page in your app's settings"""
190 | GATEWAY_GUILD_MEMBERS = 1 << 14
191 | """Intent required for bots in 100 or more servers to
192 | receive member-related events like guild_member_add.
193 | See the list of member-related events under GUILD_MEMBERS"""
194 | GATEWAY_GUILD_MEMBERS_LIMITED = 1 << 15
195 | """Intent required for bots in under 100 servers to receive member-related events
196 | like guild_member_add, found on the Bot page in your app's settings.
197 | See the list of member-related events under GUILD_MEMBERS"""
198 | VERIFICATION_PENDING_GUILD_LIMIT = 1 << 16
199 | """Indicates unusual growth of an app that prevents verification"""
200 | EMBEDDED = 1 << 17
201 | """Indicates if an app is embedded within the
202 | Discord client (currently unavailable publicly)"""
203 | GATEWAY_MESSAGE_CONTENT = 1 << 18
204 | """Intent required for bots in 100 or more servers to receive message content"""
205 | GATEWAY_MESSAGE_CONTENT_LIMITED = 1 << 19
206 | """Intent required for bots in under 100 servers to receive message content,
207 | found on the Bot page in your app's settings"""
208 | APPLICATION_COMMAND_BADGE = 1 << 23
209 | """Indicates if an app has registered global application commands"""
210 |
211 |
212 | class ApplicationRoleConnectionMetadataType(IntEnum):
213 | """Application role connection metadata type.
214 |
215 | see https://discord.com/developers/docs/resources/application-role-connection-metadata#application-role-connection-metadata-object-application-role-connection-metadata-type
216 | """
217 |
218 | INTEGER_LESS_THAN_OR_EQUAL = 1
219 | """the metadata value (integer) is less than or equal
220 | to the guild's configured value (integer)"""
221 | INTEGER_GREATER_THAN_OR_EQUAL = 2
222 | """the metadata value (integer) is greater than or equal
223 | to the guild's configured value (integer)"""
224 | INTEGER_EQUAL = 3
225 | """the metadata value (integer) is equal to the
226 | guild's configured value (integer)"""
227 | INTEGER_NOT_EQUAL = 4
228 | """ the metadata value (integer) is not equal to the
229 | guild's configured value (integer)"""
230 | DATETIME_LESS_THAN_OR_EQUAL = 5
231 | """ the metadata value (ISO8601 string) is less than or equal
232 | to the guild's configured value (integer; days before current date)"""
233 | DATETIME_GREATER_THAN_OR_EQUAL = 6
234 | """the metadata value (ISO8601 string) is greater than or equal
235 | to the guild's configured value (integer; days before current date)"""
236 | BOOLEAN_EQUAL = 7
237 | """the metadata value (integer) is equal to the
238 | guild's configured value (integer; 1)"""
239 | BOOLEAN_NOT_EQUAL = 8
240 | """the metadata value (integer) is not equal to the
241 | guild's configured value (integer; 1)"""
242 |
243 |
244 | class AllowedMentionType(StrEnum):
245 | """Allowed mentions types.
246 |
247 | see https://discord.com/developers/docs/resources/channel#allowed-mentions-object-allowed-mention-types
248 | """
249 |
250 | RoleMentions = "roles"
251 | """Controls role mentions"""
252 | UserMentions = "users"
253 | """Controls user mentions"""
254 | EveryoneMentions = "everyone"
255 | """Controls @everyone and @here mentions"""
256 |
257 |
258 | class AuditLogEventType(IntEnum):
259 | """Audit Log Event Type
260 |
261 | see https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events
262 | """
263 |
264 | GUILD_UPDATE = 1
265 | CHANNEL_CREATE = 10
266 | CHANNEL_UPDATE = 11
267 | CHANNEL_DELETE = 12
268 | CHANNEL_OVERWRITE_CREATE = 13
269 | CHANNEL_OVERWRITE_UPDATE = 14
270 | CHANNEL_OVERWRITE_DELETE = 15
271 | MEMBER_KICK = 20
272 | MEMBER_PRUNE = 21
273 | MEMBER_BAN_ADD = 22
274 | MEMBER_BAN_REMOVE = 23
275 | MEMBER_UPDATE = 24
276 | MEMBER_ROLE_UPDATE = 25
277 | MEMBER_MOVE = 26
278 | MEMBER_DISCONNECT = 27
279 | BOT_ADD = 28
280 | ROLE_CREATE = 30
281 | ROLE_UPDATE = 31
282 | ROLE_DELETE = 32
283 | INVITE_CREATE = 40
284 | INVITE_UPDATE = 41
285 | INVITE_DELETE = 42
286 | WEBHOOK_CREATE = 50
287 | WEBHOOK_UPDATE = 51
288 | WEBHOOK_DELETE = 52
289 | EMOJI_CREATE = 60
290 | EMOJI_UPDATE = 61
291 | EMOJI_DELETE = 62
292 | MESSAGE_DELETE = 72
293 | MESSAGE_BULK_DELETE = 73
294 | MESSAGE_PIN = 74
295 | MESSAGE_UNPIN = 75
296 | INTEGRATION_CREATE = 80
297 | INTEGRATION_UPDATE = 81
298 | INTEGRATION_DELETE = 82
299 | STAGE_INSTANCE_CREATE = 83
300 | STAGE_INSTANCE_UPDATE = 84
301 | STAGE_INSTANCE_DELETE = 85
302 | STICKER_CREATE = 90
303 | STICKER_UPDATE = 91
304 | STICKER_DELETE = 92
305 | GUILD_SCHEDULED_EVENT_CREATE = 100
306 | GUILD_SCHEDULED_EVENT_UPDATE = 101
307 | GUILD_SCHEDULED_EVENT_DELETE = 102
308 | THREAD_CREATE = 110
309 | THREAD_UPDATE = 111
310 | THREAD_DELETE = 112
311 | APPLICATION_COMMAND_PERMISSION_UPDATE = 121
312 | AUTO_MODERATION_RULE_CREATE = 140
313 | AUTO_MODERATION_RULE_UPDATE = 141
314 | AUTO_MODERATION_RULE_DELETE = 142
315 | AUTO_MODERATION_BLOCK_MESSAGE = 143
316 | AUTO_MODERATION_FLAG_TO_CHANNEL = 144
317 | AUTO_MODERATION_USER_COMMUNICATION_DISABLED = 145
318 |
319 |
320 | class AutoModerationActionType(IntEnum):
321 | """Auto moderation action type.
322 |
323 | see https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-action-object-action-types
324 | """
325 |
326 | BLOCK_MESSAGE = 1
327 | """blocks a member's message and prevents it from being posted.
328 | A custom explanation can be specified and shown to
329 | members whenever their message is blocked."""
330 | SEND_ALERT_MESSAGE = 2
331 | """logs user content to a specified channel"""
332 | TIMEOUT = 3
333 | """timeout user for a specified duration.
334 | A TIMEOUT action can only be set up for KEYWORD and MENTION_SPAM rules.
335 | The MODERATE_MEMBERS permission is required to use the TIMEOUT action type."""
336 |
337 |
338 | class AutoModerationRuleEventType(IntEnum):
339 | """Auto moderation rule event type.
340 |
341 | see https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-event-types
342 | """
343 |
344 | MESSAGE_SEND = 1
345 | """when a member sends or edits a message in the guild"""
346 |
347 |
348 | class ButtonStyle(IntEnum):
349 | """Button styles.
350 |
351 | see https://discord.com/developers/docs/interactions/message-components#button-object-button-styles
352 | """
353 |
354 | Primary = 1
355 | """color: blurple, required field: custom_id"""
356 | Secondary = 2
357 | """color: grey, required field: custom_id"""
358 | Success = 3
359 | """color: green, required field: custom_id"""
360 | Danger = 4
361 | """color: red, required field: custom_id"""
362 | Link = 5
363 | """color: grey, navigates to a URL, required field: url"""
364 |
365 |
366 | class ChannelFlags(IntFlag):
367 | """Channel flags.
368 |
369 | see https://discord.com/developers/docs/resources/channel#channel-object-channel-flags
370 | """
371 |
372 | PINNED = 1 << 1
373 | """this thread is pinned to the top of its parent GUILD_FORUM channel"""
374 | REQUIRE_TAG = 1 << 4
375 | """whether a tag is required to be specified
376 | when creating a thread in a GUILD_FORUM channel.
377 | Tags are specified in the applied_tags field."""
378 |
379 |
380 | class ChannelType(IntEnum):
381 | """Channel type.
382 |
383 | see https://discord.com/developers/docs/resources/channel#channel-object-channel-types
384 | """
385 |
386 | GUILD_TEXT = 0
387 | """a text channel within a server"""
388 | DM = 1
389 | """a direct message between users"""
390 | GUILD_VOICE = 2
391 | """a voice channel within a server"""
392 | GROUP_DM = 3
393 | """a direct message between multiple users"""
394 | GUILD_CATEGORY = 4
395 | """an organizational category that contains up to 50 channels"""
396 | GUILD_ANNOUNCEMENT = 5
397 | """a channel that users can follow and crosspost
398 | into their own server (formerly news channels)"""
399 | ANNOUNCEMENT_THREAD = 10
400 | """a temporary sub-channel within a GUILD_ANNOUNCEMENT channel"""
401 | PUBLIC_THREAD = 11
402 | """a temporary sub-channel within a GUILD_TEXT or GUILD_FORUM channel"""
403 | PRIVATE_THREAD = 12
404 | """a temporary sub-channel within a GUILD_TEXT channel that is only viewable by
405 | those invited and those with the MANAGE_THREADS permission"""
406 | GUILD_STAGE_VOICE = 13
407 | """a voice channel for hosting events with an audience"""
408 | GUILD_DIRECTORY = 14
409 | """the channel in a hub containing the listed servers"""
410 | GUILD_FORUM = 15
411 | """Channel that can only contain threads"""
412 |
413 |
414 | class ComponentType(IntEnum):
415 | """Component types.
416 |
417 | see https://discord.com/developers/docs/interactions/message-components#component-object-component-types
418 | """
419 |
420 | ActionRow = 1
421 | """Container for other components"""
422 | Button = 2
423 | """Button object"""
424 | StringSelect = 3
425 | """Select menu for picking from defined text options"""
426 | TextInput = 4
427 | """TextSegment input object"""
428 | UserInput = 5
429 | """Select menu for users"""
430 | RoleSelect = 6
431 | """Select menu for roles"""
432 | MentionableSelect = 7
433 | """Select menu for mentionables (users and roles)"""
434 | ChannelSelect = 8
435 | """Select menu for channels"""
436 |
437 |
438 | class ConnectionServiceType(StrEnum):
439 | """Connection service type.
440 |
441 | see https://discord.com/developers/docs/resources/user#connection-object-services"""
442 |
443 | Battle_net = "battlenet"
444 | eBay = "ebay"
445 | Epic_Games = "epicgames"
446 | Facebook = "facebook"
447 | GitHub = "gitHub"
448 | Instagram = "instagram"
449 | League_of_Legends = "leagueoflegends"
450 | PayPal = "payPal"
451 | PlayStation_Network = "playstation"
452 | Reddit = "reddit"
453 | Riot_Games = "riotgames"
454 | Spotify = "spotify"
455 | Skype = "skype"
456 | Steam = "steam"
457 | TikTok = "tiktok"
458 | Twitch = "twitch"
459 | Twitter = "twitter"
460 | Xbox_Live = "xbox"
461 | YouTube = "youtube"
462 |
463 |
464 | class DefaultMessageNotificationLevel(IntEnum):
465 | """Default message notification level.
466 |
467 | see https://discord.com/developers/docs/resources/guild#guild-object-default-message-notification-level
468 | """
469 |
470 | ALL_MESSAGES = 0
471 | """members will receive notifications for all messages by default"""
472 | ONLY_MENTIONS = 1
473 | """members will receive notifications only for messages
474 | that @mention them by default"""
475 |
476 |
477 | class EmbedTypes(StrEnum):
478 | """
479 | Embed types.
480 |
481 | see https://discord.com/developers/docs/resources/channel#embed-object-embed-types
482 | """
483 |
484 | rich = "rich"
485 | """generic embed rendered from embed attributes"""
486 | image = "image"
487 | """image embed"""
488 | video = "video"
489 | """video embed"""
490 | gifv = "gifv"
491 | """animated gif image embed rendered as a video embed"""
492 | article = "article"
493 | """article embed"""
494 | link = "link"
495 | """link embed"""
496 |
497 |
498 | class ExplicitContentFilterLevel(IntEnum):
499 | """Explicit content filter level.
500 |
501 | see https://discord.com/developers/docs/resources/guild#guild-object-explicit-content-filter-level
502 | """
503 |
504 | DISABLED = 0
505 | """media content will not be scanned"""
506 | MEMBERS_WITHOUT_ROLES = 1
507 | """media content sent by members without roles will be scanned"""
508 | ALL_MEMBERS = 2
509 | """media content sent by all members will be scanned"""
510 |
511 |
512 | class ForumLayoutTypes(IntEnum):
513 | """Forum layout types.
514 |
515 | see https://discord.com/developers/docs/resources/channel#channel-object-forum-layout-types
516 | """
517 |
518 | NOT_SET = 0
519 | """No default has been set for forum channel"""
520 | LIST_VIEW = 1
521 | """Display posts as a list"""
522 | GALLERY_VIEW = 2
523 | """Display posts as a collection of tiles"""
524 |
525 |
526 | class GuildFeature(StrEnum):
527 | """Guild feature.
528 |
529 | see https://discord.com/developers/docs/resources/guild#guild-object-guild-features
530 | """
531 |
532 | ACTIVITIES_ALPHA = "ACTIVITIES_ALPHA"
533 | ACTIVITIES_EMPLOYEE = "ACTIVITIES_EMPLOYEE"
534 | ACTIVITIES_INTERNAL_DEV = "ACTIVITIES_INTERNAL_DEV"
535 | ANIMATED_BANNER = "ANIMATED_BANNER"
536 | """guild has access to set an animated guild banner image"""
537 | ANIMATED_ICON = "ANIMATED_ICON"
538 | """guild has access to set an animated guild icon"""
539 | APPLICATION_COMMAND_PERMISSIONS_V2 = "APPLICATION_COMMAND_PERMISSIONS_V2"
540 | """guild is using the old permissions configuration behavior"""
541 | AUTO_MODERATION = "AUTO_MODERATION"
542 | """guild has set up auto moderation rules"""
543 | AUTOMOD_TRIGGER_KEYWORD_FILTER = "AUTOMOD_TRIGGER_KEYWORD_FILTER"
544 | AUTOMOD_TRIGGER_ML_SPAM_FILTER = "AUTOMOD_TRIGGER_ML_SPAM_FILTER"
545 | """Given to guilds previously in the 2022-03_automod_trigger_ml_spam_filter experiment overrides"""
546 | AUTOMOD_TRIGGER_SPAM_LINK_FILTER = "AUTOMOD_TRIGGER_SPAM_LINK_FILTER"
547 | AUTOMOD_TRIGGER_USER_PROFILE = "AUTOMOD_TRIGGER_USER_PROFILE"
548 | """Server has enabled AutoMod for user profiles"""
549 | BANNER = "BANNER"
550 | """guild has access to set a guild banner image"""
551 | BFG = "BFG"
552 | """Internally documented as big funky guild"""
553 | BOOSTING_TIERS_EXPERIMENT_MEDIUM_GUILD = "BOOSTING_TIERS_EXPERIMENT_MEDIUM_GUILD"
554 | BOOSTING_TIERS_EXPERIMENT_SMALL_GUILD = "BOOSTING_TIERS_EXPERIMENT_SMALL_GUILD"
555 | BOT_DEVELOPER_EARLY_ACCESS = "BOT_DEVELOPER_EARLY_ACCESS"
556 | """Enables early access features for bot and library developers"""
557 | BURST_REACTIONS = "BURST_REACTIONS"
558 | """Enables burst reactions for the guild"""
559 | CHANNEL_EMOJIS_GENERATED = "CHANNEL_EMOJIS_GENERATED"
560 | CHANNEL_HIGHLIGHTS = "CHANNEL_HIGHLIGHTS"
561 | CHANNEL_HIGHLIGHTS_DISABLED = "CHANNEL_HIGHLIGHTS_DISABLED"
562 | CHANNEL_ICON_EMOJIS_GENERATED = "CHANNEL_ICON_EMOJIS_GENERATED"
563 | CLAN = "CLAN"
564 | """The server is a clan server"""
565 | CLYDE_DISABLED = "CLYDE_DISABLED"
566 | """Given when a server administrator disables ClydeAI for the guild"""
567 | CLYDE_ENABLED = "CLYDE_ENABLED"
568 | """Server has enabled Clyde AI"""
569 | CLYDE_EXPERIMENT_ENABLED = "CLYDE_EXPERIMENT_ENABLED"
570 | """Enables ClydeAI for the guild"""
571 | COMMUNITY = "COMMUNITY"
572 | """guild can enable welcome screen, Membership Screening,
573 | stage channels and discovery, and receives community updates"""
574 | COMMUNITY_CANARY = "COMMUNITY_CANARY"
575 | COMMUNITY_EXP_LARGE_GATED = "COMMUNITY_EXP_LARGE_GATED"
576 | COMMUNITY_EXP_LARGE_UNGATED = "COMMUNITY_EXP_LARGE_UNGATED"
577 | COMMUNITY_EXP_MEDIUM = "COMMUNITY_EXP_MEDIUM"
578 | CREATOR_ACCEPTED_NEW_TERMS = "CREATOR_ACCEPTED_NEW_TERMS"
579 | """The server owner accepted the new monetization terms"""
580 | CREATOR_MONETIZABLE = "CREATOR_MONETIZABLE"
581 | """Given to guilds that enabled role subscriptions through the manual approval system"""
582 | CREATOR_MONETIZABLE_DISABLED = "CREATOR_MONETIZABLE_DISABLED"
583 | CREATOR_MONETIZABLE_PENDING_NEW_OWNER_ONBOARDING = (
584 | "CREATOR_MONETIZABLE_PENDING_NEW_OWNER_ONBOARDING"
585 | )
586 | CREATOR_MONETIZABLE_RESTRICTED = "CREATOR_MONETIZABLE_RESTRICTED"
587 | CREATOR_MONETIZABLE_WHITEGLOVE = "CREATOR_MONETIZABLE_WHITEGLOVE"
588 | CREATOR_MONETIZABLE_PROVISIONAL = "CREATOR_MONETIZABLE_PROVISIONAL"
589 | """guild has enabled monetization"""
590 | CREATOR_MONETIZATION_APPLICATION_ALLOWLIST = (
591 | "CREATOR_MONETIZATION_APPLICATION_ALLOWLIST"
592 | )
593 | CREATOR_STORE_PAGE = "CREATOR_STORE_PAGE"
594 | """guild has enabled the role subscription promo page"""
595 | DEVELOPER_SUPPORT_SERVER = "DEVELOPER_SUPPORT_SERVER"
596 | """guild has been set as a support server on the App Directory"""
597 | DISCOVERABLE = "DISCOVERABLE"
598 | """guild is able to be discovered in the directory"""
599 | DISCOVERABLE_DISABLED = "DISCOVERABLE_DISABLED"
600 | """Guild is permanently removed from Discovery by Discord"""
601 | ENABLED_DISCOVERABLE_BEFORE = "ENABLED_DISCOVERABLE_BEFORE"
602 | """Given to servers that have enabled Discovery at any point"""
603 | ENABLED_MODERATION_EXPERIENCE_FOR_NON_COMMUNITY = (
604 | "ENABLED_MODERATION_EXPERIENCE_FOR_NON_COMMUNITY"
605 | )
606 | """Moves the member list from the guild settings to the member tab for non-community guilds"""
607 | EXPOSED_TO_ACTIVITIES_WTP_EXPERIMENT = "EXPOSED_TO_ACTIVITIES_WTP_EXPERIMENT"
608 | """Given to guilds previously in the 2021-11_activities_baseline_engagement_bundle experiment overrides"""
609 | FEATURABLE = "FEATURABLE"
610 | """guild is able to be featured in the directory"""
611 | GUESTS_ENABLED = "GUESTS_ENABLED"
612 | """Guild has used guest invites"""
613 | GUILD_AUTOMOD_DEFAULT_LIST = "GUILD_AUTOMOD_DEFAULT_LIST"
614 | """Given to guilds in the 2022-03_guild_automod_default_list experiment overrides"""
615 | GUILD_COMMUNICATION_DISABLED_GUILDS = "GUILD_COMMUNICATION_DISABLED_GUILDS"
616 | """Given to guilds previously in the 2021-11_guild_communication_disabled_guilds experiment overrides"""
617 | GUILD_HOME_DEPRECATION_OVERRIDE = "GUILD_HOME_DEPRECATION_OVERRIDE"
618 | GUILD_HOME_OVERRIDE = "GUILD_HOME_OVERRIDE"
619 | """Gives the guild access to the Home feature, enables Treatment 2 of the 2022-01_home_tab_guild experiment overrides"""
620 | GUILD_HOME_TEST = "GUILD_HOME_TEST"
621 | """Gives the guild access to the Home feature, enables Treatment 1 of the 2022-01_home_tab_guild experiment"""
622 | GUILD_MEMBER_VERIFICATION_EXPERIMENT = "GUILD_MEMBER_VERIFICATION_EXPERIMENT"
623 | """Given to guilds previously in the 2021-11_member_verification_manual_approval experiment"""
624 | GUILD_ONBOARDING = "GUILD_ONBOARDING"
625 | """Guild has enabled onboarding"""
626 | GUILD_ONBOARDING_ADMIN_ONLY = "GUILD_ONBOARDING_ADMIN_ONLY"
627 | GUILD_ONBOARDING_EVER_ENABLED = "GUILD_ONBOARDING_EVER_ENABLED"
628 | """Guild has ever enabled onboarding"""
629 | GUILD_ONBOARDING_HAS_PROMPTS = "GUILD_ONBOARDING_HAS_PROMPTS"
630 | GUILD_PRODUCTS = "GUILD_PRODUCTS"
631 | """Given to guilds previously in the 2023-04_server_products experiment overrides"""
632 | GUILD_PRODUCTS_ALLOW_ARCHIVED_FILE = "GUILD_PRODUCTS_ALLOW_ARCHIVED_FILE"
633 | GUILD_ROLE_SUBSCRIPTIONS = "GUILD_ROLE_SUBSCRIPTIONS"
634 | """Given to guilds previously in the 2021-06_guild_role_subscriptions experiment overrides"""
635 | GUILD_ROLE_SUBSCRIPTION_PURCHASE_FEEDBACK_LOOP = (
636 | "GUILD_ROLE_SUBSCRIPTION_PURCHASE_FEEDBACK_LOOP"
637 | )
638 | """Given to guilds previously in the 2022-05_mobile_web_role_subscription_purchase_page experiment overrides"""
639 | GUILD_ROLE_SUBSCRIPTION_TIER_TEMPLATE = "GUILD_ROLE_SUBSCRIPTION_TIER_TEMPLATE"
640 | GUILD_ROLE_SUBSCRIPTION_TRIALS = "GUILD_ROLE_SUBSCRIPTION_TRIALS"
641 | """Given to guilds previously in the 2022-01_guild_role_subscription_trials experiment overrides"""
642 | GUILD_SERVER_GUIDE = "GUILD_SERVER_GUIDE"
643 | """Guild has enabled server guide"""
644 | GUILD_WEB_PAGE_VANITY_URL = "GUILD_WEB_PAGE_VANITY_URL"
645 | HAD_EARLY_ACTIVITIES_ACCESS = "HAD_EARLY_ACTIVITIES_ACCESS"
646 | """Server previously had access to voice channel activities and can bypass the boost level requirement"""
647 | HAS_DIRECTORY_ENTRY = "HAS_DIRECTORY_ENTRY"
648 | """Guild is in a directory channel"""
649 | HIDE_FROM_EXPERIMENT_UI = "HIDE_FROM_EXPERIMENT_UI"
650 | HUB = "HUB"
651 | """Student Hubs contain a directory channel that let you find school-related, student-run servers for your school or university"""
652 | INCREASED_THREAD_LIMIT = "INCREASED_THREAD_LIMIT"
653 | """Allows the server to have 1,000+ active threads"""
654 | INTERNAL_EMPLOYEE_ONLY = "INTERNAL_EMPLOYEE_ONLY"
655 | """Restricts the guild so that only users with the staff flag can join"""
656 | INVITES_DISABLED = "INVITES_DISABLED"
657 | """guild has paused invites, preventing new users from joining"""
658 | INVITE_SPLASH = "INVITE_SPLASH"
659 | """guild has access to set an invite splash background"""
660 | LINKED_TO_HUB = "LINKED_TO_HUB"
661 | MARKETPLACES_CONNECTION_ROLES = "MARKETPLACES_CONNECTION_ROLES"
662 | MEMBER_PROFILES = "MEMBER_PROFILES"
663 | """Allows members to customize their avatar, banner and bio for that server"""
664 | MEMBER_SAFETY_PAGE_ROLLOUT = "MEMBER_SAFETY_PAGE_ROLLOUT"
665 | """Assigns the experiment of the Member Safety panel and lockdowns to the guild"""
666 | MEMBER_VERIFICATION_GATE_ENABLED = "MEMBER_VERIFICATION_GATE_ENABLED"
667 | """guild has enabled Membership Screening"""
668 | MEMBER_VERIFICATION_MANUAL_APPROVAL = "MEMBER_VERIFICATION_MANUAL_APPROVAL"
669 | MOBILE_WEB_ROLE_SUBSCRIPTION_PURCHASE_PAGE = (
670 | "MOBILE_WEB_ROLE_SUBSCRIPTION_PURCHASE_PAGE"
671 | )
672 | """Given to guilds previously in the 2022-05_mobile_web_role_subscription_purchase_page experiment overrides"""
673 | MONETIZATION_ENABLED = "MONETIZATION_ENABLED"
674 | """Allows the server to set a team in dev portal to receive role subscription payouts"""
675 | MORE_EMOJI = "MORE_EMOJI"
676 | """Adds 150 extra emoji slots to each category (normal and animated emoji). Not used in server boosting"""
677 | MORE_STICKERS = "MORE_STICKERS"
678 | """guild has increased custom sticker slots"""
679 | NEWS = "NEWS"
680 | """guild has access to create announcement channels"""
681 | NEW_THREAD_PERMISSIONS = "NEW_THREAD_PERMISSIONS"
682 | """Guild has new thread permissions"""
683 | NON_COMMUNITY_RAID_ALERTS = "NON_COMMUNITY_RAID_ALERTS"
684 | """Non-community guild is opt-in to raid alerts"""
685 | PARTNERED = "PARTNERED"
686 | """guild is partnered"""
687 | PREMIUM_TIER_3_OVERRIDE = "PREMIUM_TIER_3_OVERRIDE"
688 | """Forces the server to server boosting level 3"""
689 | PREVIEW_ENABLED = "PREVIEW_ENABLED"
690 | """guild can be previewed before joining via
691 | Membership Screening or the directory"""
692 | PRODUCTS_AVAILABLE_FOR_PURCHASE = "PRODUCTS_AVAILABLE_FOR_PURCHASE"
693 | """Guild has server products available for purchase"""
694 | RAID_ALERTS_DISABLED = "RAID_ALERTS_DISABLED"
695 | """Guild is opt-out from raid alerts"""
696 | RELAY_ENABLED = "RELAY_ENABLED"
697 | """Shards connections to the guild to different nodes that relay information between each other"""
698 | RESTRICT_SPAM_RISK_GUILDS = "RESTRICT_SPAM_RISK_GUILDS"
699 | ROLE_ICONS = "ROLE_ICONS"
700 | """guild is able to set role icons"""
701 | ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE = (
702 | "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE"
703 | )
704 | """guild has role subscriptions that can be purchased"""
705 | ROLE_SUBSCRIPTIONS_ENABLED = "ROLE_SUBSCRIPTIONS_ENABLED"
706 | """guild has enabled role subscriptions"""
707 | ROLE_SUBSCRIPTIONS_ENABLED_FOR_PURCHASE = "ROLE_SUBSCRIPTIONS_ENABLED_FOR_PURCHASE"
708 | SHARD = "SHARD"
709 | SHARED_CANVAS_FRIENDS_AND_FAMILY_TEST = "SHARED_CANVAS_FRIENDS_AND_FAMILY_TEST"
710 | """Given to guilds previously in the 2023-01_shared_canvas experiment overrides"""
711 | SOUNDBOARD = "SOUNDBOARD"
712 | SUMMARIES_DISABLED_BY_USER = "SUMMARIES_DISABLED_BY_USER"
713 | SUMMARIES_ENABLED = "SUMMARIES_ENABLED"
714 | """Given to guilds in the 2023-02_p13n_summarization experiment overrides"""
715 | SUMMARIES_ENABLED_BY_USER = "SUMMARIES_ENABLED_BY_USER"
716 | SUMMARIES_ENABLED_GA = "SUMMARIES_ENABLED_GA"
717 | """Given to guilds in the 2023-02_p13n_summarization experiment overrides"""
718 | SUMMARIES_LONG_LOOKBACK = "SUMMARIES_LONG_LOOKBACK"
719 | SUMMARIES_OPT_OUT_EXPERIENCE = "SUMMARIES_OPT_OUT_EXPERIENCE"
720 | SUMMARIES_PAUSED = "SUMMARIES_PAUSED"
721 | STAFF_LEVEL_COLLABORATOR_REQUIRED = "STAFF_LEVEL_COLLABORATOR_REQUIRED"
722 | STAFF_LEVEL_RESTRICTED_COLLABORATOR_REQUIRED = (
723 | "STAFF_LEVEL_RESTRICTED_COLLABORATOR_REQUIRED"
724 | )
725 | TEXT_IN_STAGE_ENABLED = "TEXT_IN_STAGE_ENABLED"
726 | TEXT_IN_VOICE_ENABLED = "TEXT_IN_VOICE_ENABLED"
727 | """Show a chat button inside voice channels that opens a dedicated text channel in a sidebar similar to thread view"""
728 | THREADS_ENABLED_TESTING = "THREADS_ENABLED_TESTING"
729 | """Used by bot developers to test their bots with threads in guilds with 5 or less members and a bot. Also gives the premium thread features"""
730 | THREADS_ENABLED = "THREADS_ENABLED"
731 | """Enabled threads early access"""
732 | THREAD_DEFAULT_AUTO_ARCHIVE_DURATION = "THREAD_DEFAULT_AUTO_ARCHIVE_DURATION"
733 | """Unknown, presumably used for testing changes to the thread default auto archive duration"""
734 | THREADS_ONLY_CHANNEL = "THREADS_ONLY_CHANNEL"
735 | """Given to guilds previously in the 2021-07_threads_only_channel experiment overrides"""
736 | TICKETED_EVENTS_ENABLED = "TICKETED_EVENTS_ENABLED"
737 | """guild has enabled ticketed events"""
738 | TICKETING_ENABLED = "TICKETING_ENABLED"
739 | VANITY_URL = "VANITY_URL"
740 | """guild has access to set a vanity URL"""
741 | VERIFIED = "VERIFIED"
742 | """guild is verified"""
743 | VIP_REGIONS = "VIP_REGIONS"
744 | """guild has access to set 384kbps bitrate in voice
745 | (previously VIP voice servers)"""
746 | VOICE_CHANNEL_EFFECTS = "VOICE_CHANNEL_EFFECTS"
747 | """Given to guilds previously in the 2022-06_voice_channel_effects experiment overrides"""
748 | VOICE_IN_THREADS = "VOICE_IN_THREADS"
749 | WELCOME_SCREEN_ENABLED = "WELCOME_SCREEN_ENABLED"
750 | """guild has enabled the welcome screen"""
751 |
752 |
753 | class GuildMemberFlags(IntFlag):
754 | """Guild member flags.
755 |
756 | see https://discord.com/developers/docs/resources/guild#guild-member-object-guild-member-flags
757 | """
758 |
759 | DID_REJOIN = 1 << 0
760 | """Member has left and rejoined the guild"""
761 | COMPLETED_ONBOARDING = 1 << 1
762 | """Member has completed onboarding"""
763 | BYPASSES_VERIFICATION = 1 << 2
764 | """Member is exempt from guild verification requirements"""
765 | STARTED_ONBOARDING = 1 << 3
766 | """Member has started onboarding"""
767 |
768 |
769 | class GuildNSFWLevel(IntEnum):
770 | """Guild NSFW level.
771 |
772 | see https://discord.com/developers/docs/resources/guild#guild-object-guild-nsfw-level
773 | """
774 |
775 | DEFAULT = 0
776 | EXPLICIT = 1
777 | SAFE = 2
778 | AGE_RESTRICTED = 3
779 |
780 |
781 | class GuildScheduledEventEntityType(IntEnum):
782 | """Guild Scheduled Event Entity Type
783 |
784 | see https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-entity-types
785 | """
786 |
787 | STAGE_INSTANCE = 1
788 | VOICE = 2
789 | EXTERNAL = 3
790 |
791 |
792 | class GuildScheduledEventPrivacyLevel(IntEnum):
793 | """Guild Scheduled Event Privacy Level
794 |
795 | see https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-privacy-level
796 | """
797 |
798 | GUILD_ONLY = 2
799 |
800 |
801 | class GuildScheduledEventStatus(IntEnum):
802 | """Guild Scheduled Event Status
803 |
804 | Once status is set to COMPLETED or CANCELED, the status can no longer be updated.
805 |
806 | see https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-status
807 | """
808 |
809 | SCHEDULED = 1
810 | ACTIVE = 2
811 | COMPLETED = 3
812 | CANCELED = 4
813 |
814 |
815 | class IntegrationExpireBehaviors(IntEnum):
816 | """Integration Expire Behaviors
817 |
818 | see https://discord.com/developers/docs/resources/guild#integration-object-integration-expire-behaviors
819 | """
820 |
821 | RemoveRole = 0
822 | Kick = 0
823 |
824 |
825 | class InteractionType(IntEnum):
826 | """Interaction type.
827 |
828 | see https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-type
829 | """
830 |
831 | PING = 1
832 | APPLICATION_COMMAND = 2
833 | MESSAGE_COMPONENT = 3
834 | APPLICATION_COMMAND_AUTOCOMPLETE = 4
835 | MODAL_SUBMIT = 5
836 |
837 |
838 | class InteractionCallbackType(IntEnum):
839 | """Interaction callback type.
840 |
841 | see https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-interaction-callback-type
842 | """
843 |
844 | PONG = 1
845 | """ACK a Ping"""
846 | CHANNEL_MESSAGE_WITH_SOURCE = 4
847 | """respond to an interaction with a message"""
848 | DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5
849 | """ACK an interaction and edit a response later, the user sees a loading state"""
850 | DEFERRED_UPDATE_MESSAGE = 6
851 | """for components, ACK an interaction and edit the original message later;
852 | the user does not see a loading state"""
853 | UPDATE_MESSAGE = 7
854 | """for components, edit the message the component was attached to.
855 | Only valid for component-based interactions"""
856 | APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8
857 | """respond to an autocomplete interaction with suggested choices"""
858 | MODAL = 9
859 | """respond to an interaction with a popup modal.
860 | Not available for MODAL_SUBMIT and PING interactions."""
861 |
862 |
863 | class InviteTargetType(IntEnum):
864 | """Invite target type.
865 |
866 | see https://discord.com/developers/docs/resources/invite#invite-object-invite-target-types
867 | """
868 |
869 | STREAM = 1
870 | EMBEDDED_APPLICATION = 2
871 |
872 |
873 | class KeywordPresetType(IntEnum):
874 | """Keyword preset type.
875 |
876 | see https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-keyword-preset-types
877 | """
878 |
879 | PROFANITY = 1
880 | """words that may be considered forms of swearing or cursing"""
881 | SEXUAL_CONTENT = 2
882 | """"words that refer to sexually explicit behavior or activity"""
883 | SLURS = 3
884 | """personal insults or words that may be considered hate speech"""
885 |
886 |
887 | class MessageActivityType(IntEnum):
888 | """Message activity type.
889 |
890 | see https://discord.com/developers/docs/resources/channel#message-object-message-activity-types
891 | """
892 |
893 | JOIN = 1
894 | SPECTATE = 2
895 | LISTEN = 3
896 | JOIN_REQUEST = 5
897 |
898 |
899 | class MessageFlag(IntFlag):
900 | """Message flags.
901 |
902 | see https://discord.com/developers/docs/resources/channel#message-object-message-flags
903 | """
904 |
905 | CROSSPOSTED = 1 << 0
906 | """this message has been published to subscribed channels (via Channel Following)"""
907 | IS_CROSSPOST = 1 << 1
908 | """this message originated from a message in
909 | another channel (via Channel Following)"""
910 | SUPPRESS_EMBEDS = 1 << 2
911 | """do not include any embeds when serializing this message"""
912 | SOURCE_MESSAGE_DELETED = 1 << 3
913 | """the source message for this crosspost has been deleted (via Channel Following)"""
914 | URGENT = 1 << 4
915 | """this message came from the urgent message system"""
916 | HAS_THREAD = 1 << 5
917 | """this message has an associated thread, with the same id as the message"""
918 | EPHEMERAL = 1 << 6
919 | """this message is only visible to the user who invoked the Interaction"""
920 | LOADING = 1 << 7
921 | """this message is an Interaction Response and the bot is "thinking" """
922 | FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8
923 | """this message failed to mention some roles and add their members to the thread"""
924 | SUPPRESS_NOTIFICATIONS = 1 << 12
925 | """ this message will not trigger push and desktop notifications"""
926 |
927 |
928 | class MessageType(IntEnum):
929 | """Type REPLY(19) and CHAT_INPUT_COMMAND(20) are only available in API v8 and above.
930 | In v6, they are represented as type DEFAULT(0).
931 | Additionally, type THREAD_STARTER_MESSAGE(21) is only available in API v9 and above.
932 |
933 | see https://discord.com/developers/docs/resources/channel#message-object-message-types
934 | """
935 |
936 | DEFAULT = 0
937 | RECIPIENT_ADD = 1
938 | RECIPIENT_REMOVE = 2
939 | CALL = 3
940 | CHANNEL_NAME_CHANGE = 4
941 | CHANNEL_ICON_CHANGE = 5
942 | CHANNEL_PINNED_MESSAGE = 6
943 | USER_JOIN = 7
944 | GUILD_BOOST = 8
945 | GUILD_BOOST_TIER_1 = 9
946 | GUILD_BOOST_TIER_2 = 10
947 | GUILD_BOOST_TIER_3 = 11
948 | CHANNEL_FOLLOW_ADD = 12
949 | GUILD_DISCOVERY_DISQUALIFIED = 14
950 | GUILD_DISCOVERY_REQUALIFIED = 15
951 | GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16
952 | GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17
953 | THREAD_CREATED = 18
954 | REPLY = 19
955 | CHAT_INPUT_COMMAND = 20
956 | THREAD_STARTER_MESSAGE = 21
957 | GUILD_INVITE_REMINDER = 22
958 | CONTEXT_MENU_COMMAND = 23
959 | AUTO_MODERATION_ACTION = 24
960 | ROLE_SUBSCRIPTION_PURCHASE = 25
961 | INTERACTION_PREMIUM_UPSELL = 26
962 | STAGE_START = 27
963 | STAGE_END = 28
964 | STAGE_SPEAKER = 29
965 | STAGE_TOPIC = 31
966 | GUILD_APPLICATION_PREMIUM_SUBSCRIPTION = 32
967 |
968 |
969 | class MembershipState(IntEnum):
970 | """Membership state.
971 |
972 | see https://discord.com/developers/docs/topics/teams#data-models-membership-state-enum
973 | """
974 |
975 | INVITED = 1
976 | ACCEPTED = 2
977 |
978 |
979 | class MFALevel(IntEnum):
980 | """MFA level.
981 |
982 | see https://discord.com/developers/docs/resources/guild#guild-object-mfa-level"""
983 |
984 | NONE = 0
985 | """guild has no MFA/2FA requirement for moderation actions"""
986 | ELEVATED = 1
987 | """guild has a 2FA requirement for moderation actions"""
988 |
989 |
990 | class MutableGuildFeature(StrEnum):
991 | """Mutable guild feature.
992 |
993 | see https://discord.com/developers/docs/resources/guild#guild-object-mutable-guild-features
994 | """
995 |
996 | COMMUNITY = "COMMUNITY"
997 | """Enables Community Features in the guild"""
998 | INVITES_DISABLED = "INVITES_DISABLED"
999 | """Pauses all invites/access to the server"""
1000 | DISCOVERABLE = "DISCOVERABLE"
1001 | """Enables discovery in the guild, making it publicly listed"""
1002 |
1003 |
1004 | class OnboardingPromptType(IntEnum):
1005 | """Onboarding prompt type.
1006 |
1007 | see https://discord.com/developers/docs/resources/guild#guild-onboarding-object-prompt-types
1008 | """
1009 |
1010 | MULTIPLE_CHOICE = 0
1011 | DROPDOWN = 1
1012 |
1013 |
1014 | class OverwriteType(IntEnum):
1015 | """Overwrite type.
1016 |
1017 | see https://discord.com/developers/docs/resources/channel#overwrite-object"""
1018 |
1019 | ROLE = 0
1020 | MEMBER = 1
1021 |
1022 |
1023 | class PremiumTier(IntEnum):
1024 | """Premium tier.
1025 |
1026 | see https://discord.com/developers/docs/resources/guild#guild-object-premium-tier"""
1027 |
1028 | NONE = 0
1029 | """guild has not unlocked any Server Boost perks"""
1030 | TIER_1 = 1
1031 | """guild has unlocked Server Boost level 1 perks"""
1032 | TIER_2 = 2
1033 | """guild has unlocked Server Boost level 2 perks"""
1034 | TIER_3 = 3
1035 | """guild has unlocked Server Boost level 3 perks"""
1036 |
1037 |
1038 | class PremiumType(IntEnum):
1039 | """Premium types denote the level of premium a user has.
1040 | Visit the Nitro page to learn more about the premium plans we currently offer.
1041 |
1042 | see https://discord.com/developers/docs/resources/user#user-object-premium-types"""
1043 |
1044 | NONE = 0
1045 | NITRO_CLASSIC = 1
1046 | NITRO = 2
1047 | NITRO_BASIC = 3
1048 |
1049 |
1050 | class PresenceStatus(StrEnum):
1051 | """Presence Status
1052 |
1053 | see https://discord.com/developers/docs/topics/gateway-events#presence-update-presence-update-event-fields
1054 | """
1055 |
1056 | ONLINE = "online"
1057 | DND = "dnd"
1058 | IDLE = "idle"
1059 | OFFLINE = "offline"
1060 |
1061 |
1062 | class SortOrderTypes(IntEnum):
1063 | """Sort order types.
1064 |
1065 | see https://discord.com/developers/docs/resources/channel#channel-object-sort-order-types
1066 | """
1067 |
1068 | LATEST_ACTIVITY = 0
1069 | """Sort forum posts by activity"""
1070 | CREATION_DATE = 1
1071 | """Sort forum posts by creation time (from most recent to oldest)"""
1072 |
1073 |
1074 | class StagePrivacyLevel(IntEnum):
1075 | """Stage Privacy Level
1076 |
1077 | see https://discord.com/developers/docs/resources/stage-instance#stage-instance-object-privacy-level
1078 | """
1079 |
1080 | PUBLIC = 1
1081 | """The Stage instance is visible publicly. (deprecated)"""
1082 | GUILD_ONLY = 2
1083 | """The Stage instance is visible to only guild members."""
1084 |
1085 |
1086 | class StickerFormatType(IntEnum):
1087 | """Sticker format type.
1088 |
1089 | see https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types
1090 | """
1091 |
1092 | PNG = 1
1093 | APNG = 2
1094 | LOTTIE = 3
1095 | GIF = 4
1096 |
1097 |
1098 | class StickerType(IntEnum):
1099 | """Sticker type.
1100 |
1101 | see https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types
1102 | """
1103 |
1104 | STANDARD = 1
1105 | """an official sticker in a pack, part of Nitro or in a removed purchasable pack"""
1106 | GUILD = 2
1107 | """a sticker uploaded to a guild for the guild's members"""
1108 |
1109 |
1110 | class SystemChannelFlags(IntFlag):
1111 | """System channel flags.
1112 |
1113 | see https://discord.com/developers/docs/resources/guild#guild-object-system-channel-flags
1114 | """
1115 |
1116 | SUPPRESS_JOIN_NOTIFICATIONS = 1 << 0
1117 | """Suppress member join notifications"""
1118 | SUPPRESS_PREMIUM_SUBSCRIPTIONS = 1 << 1
1119 | """Suppress server boost notifications"""
1120 | SUPPRESS_GUILD_REMINDER_NOTIFICATIONS = 1 << 2
1121 | """Suppress server setup tips"""
1122 | SUPPRESS_JOIN_NOTIFICATION_REPLIES = 1 << 3
1123 | """Hide member join sticker reply buttons"""
1124 | SUPPRESS_ROLE_SUBSCRIPTION_PURCHASE_NOTIFICATIONS = 1 << 4
1125 | """Suppress role subscription purchase and renewal notifications"""
1126 | SUPPRESS_ROLE_SUBSCRIPTION_PURCHASE_NOTIFICATION_REPLIES = 1 << 5
1127 | """Hide role subscription sticker reply buttons"""
1128 |
1129 |
1130 | class TextInputStyle(IntEnum):
1131 | """TextSegment input style.
1132 |
1133 | see https://discord.com/developers/docs/interactions/message-components#text-input-object-text-input-styles
1134 | """
1135 |
1136 | Short = 1
1137 | """Single-line input"""
1138 | Paragraph = 2
1139 | """Multi-line input"""
1140 |
1141 |
1142 | class TimeStampStyle(Enum):
1143 | """Timestamp style.
1144 |
1145 | see https://discord.com/developers/docs/reference#message-formatting-timestamp-styles
1146 | """
1147 |
1148 | ShortTime = "t"
1149 | """16:20"""
1150 | LongTime = "T"
1151 | """16:20:30"""
1152 | ShortDate = "d"
1153 | """20/04/2021"""
1154 | LongDate = "D"
1155 | """20 April 2021"""
1156 | ShortDateTime = "f"
1157 | """20 April 2021 16:20"""
1158 | LongDateTime = "F"
1159 | """Tuesday, 20 April 2021 16:20"""
1160 | RelativeTime = "R"
1161 | """2 months ago"""
1162 |
1163 |
1164 | class TriggerType(IntEnum):
1165 | """Trigger type.
1166 |
1167 | see https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-trigger-types
1168 | """
1169 |
1170 | KEYWORD = 1
1171 | """check if content contains words from a user defined list of keywords"""
1172 | SPAM = 3
1173 | """check if content represents generic spam"""
1174 | KEYWORD_PRESET = 4
1175 | """check if content contains words from internal pre-defined wordsets"""
1176 | MENTION_SPAM = 5
1177 | """check if content contains more unique mentions than allowed"""
1178 |
1179 |
1180 | class UpdatePresenceStatusType(StrEnum):
1181 | """Update Presence Status type.
1182 |
1183 | see https://discord.com/developers/docs/topics/gateway-events#update-presence-status-types
1184 | """
1185 |
1186 | online = "online"
1187 | """Online"""
1188 | dnd = "dnd"
1189 | """Do Not Disturb"""
1190 | idle = "idle"
1191 | """AFK"""
1192 | invisible = "invisible"
1193 | """Invisible and shown as offline"""
1194 | offline = "offline"
1195 | """ Offline"""
1196 |
1197 |
1198 | class UserFlags(IntFlag):
1199 | """User flags denote certain attributes about a user.
1200 | These flags are only available to bots.
1201 |
1202 | see https://discord.com/developers/docs/resources/user#user-object-user-flags"""
1203 |
1204 | STAFF = 1 << 0
1205 | """Discord Employee"""
1206 | PARTNER = 1 << 1
1207 | """Partnered Server Owner"""
1208 | HYPESQUAD = 1 << 2
1209 | """HypeSquad Events Member"""
1210 | BUG_HUNTER_LEVEL_1 = 1 << 3
1211 | """Bug Hunter Level 1"""
1212 | HYPESQUAD_ONLINE_HOUSE_1 = 1 << 6
1213 | """House Bravery Member"""
1214 | HYPESQUAD_ONLINE_HOUSE_2 = 1 << 7
1215 | """House Brilliance Member"""
1216 | HYPESQUAD_ONLINE_HOUSE_3 = 1 << 8
1217 | """House Balance Member"""
1218 | PREMIUM_EARLY_SUPPORTER = 1 << 9
1219 | """Early Nitro Supporter"""
1220 | TEAM_PSEUDO_USER = 1 << 10
1221 | """User is a team"""
1222 | BUG_HUNTER_LEVEL_2 = 1 << 14
1223 | """Bug Hunter Level 2"""
1224 | VERIFIED_BOT = 1 << 16
1225 | """Verified Bot"""
1226 | VERIFIED_DEVELOPER = 1 << 17
1227 | """Early Verified Bot Developer"""
1228 | CERTIFIED_MODERATOR = 1 << 18
1229 | """Moderator Programs Alumni"""
1230 | BOT_HTTP_INTERACTIONS = 1 << 19
1231 | """Bot uses only HTTP interactions and is shown in the online member list"""
1232 | ACTIVE_DEVELOPER = 1 << 22
1233 | """User is an Active Developer"""
1234 |
1235 |
1236 | class VerificationLevel(IntEnum):
1237 | """Verification level.
1238 |
1239 | see https://discord.com/developers/docs/resources/guild#guild-object-verification-level
1240 | """
1241 |
1242 | NONE = 0
1243 | """unrestricted"""
1244 | LOW = 1
1245 | """must have verified email on account"""
1246 | MEDIUM = 2
1247 | """must be registered on Discord for longer than 5 minutes"""
1248 | HIGH = 3
1249 | """must be a member of the server for longer than 10 minutes"""
1250 | VERY_HIGH = 4
1251 | """must have a verified phone number"""
1252 |
1253 |
1254 | class VideoQualityMode(IntEnum):
1255 | """Video quality mode.
1256 |
1257 | see https://discord.com/developers/docs/resources/channel#channel-object-video-quality-modes
1258 | """
1259 |
1260 | AUTO = 1
1261 | """Discord chooses the quality for optimal performance"""
1262 | FULL = 2
1263 | """720p"""
1264 |
1265 |
1266 | class VisibilityType(IntEnum):
1267 | """Visibility type.
1268 |
1269 | see https://discord.com/developers/docs/resources/user#connection-object-visibility-types
1270 | """
1271 |
1272 | NONE = 0
1273 | """invisible to everyone except the user themselves"""
1274 | EVERYONE = 1
1275 | """visible to everyone"""
1276 |
1277 |
1278 | class WebhookType(IntEnum):
1279 | """Webhook type.
1280 |
1281 | see https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types
1282 | """
1283 |
1284 | Incoming = 1
1285 | """ Incoming Webhooks can post messages to channels with a generated token"""
1286 | Channel_Follower = 2
1287 | """ Channel Follower Webhooks are internal webhooks used with Channel
1288 | Following to post new messages into channels"""
1289 | Application = 3
1290 | """Application webhooks are webhooks used with Interactions"""
1291 |
1292 |
1293 | __all__ = [
1294 | "UNSET",
1295 | "Missing",
1296 | "MissingOrNullable",
1297 | "ActivityAssetImage",
1298 | "ActivityFlags",
1299 | "ActivityType",
1300 | "ApplicationCommandOptionType",
1301 | "ApplicationCommandPermissionsType",
1302 | "ApplicationCommandType",
1303 | "ApplicationFlag",
1304 | "ApplicationRoleConnectionMetadataType",
1305 | "AllowedMentionType",
1306 | "AuditLogEventType",
1307 | "AutoModerationActionType",
1308 | "AutoModerationRuleEventType",
1309 | "ButtonStyle",
1310 | "ChannelFlags",
1311 | "ChannelType",
1312 | "ComponentType",
1313 | "ConnectionServiceType",
1314 | "DefaultMessageNotificationLevel",
1315 | "EmbedTypes",
1316 | "ExplicitContentFilterLevel",
1317 | "ForumLayoutTypes",
1318 | "GuildFeature",
1319 | "GuildMemberFlags",
1320 | "GuildNSFWLevel",
1321 | "GuildScheduledEventEntityType",
1322 | "GuildScheduledEventPrivacyLevel",
1323 | "GuildScheduledEventStatus",
1324 | "IntegrationExpireBehaviors",
1325 | "InteractionType",
1326 | "InteractionCallbackType",
1327 | "InviteTargetType",
1328 | "KeywordPresetType",
1329 | "MessageActivityType",
1330 | "MessageFlag",
1331 | "MessageType",
1332 | "MembershipState",
1333 | "MFALevel",
1334 | "MutableGuildFeature",
1335 | "OnboardingPromptType",
1336 | "OverwriteType",
1337 | "PremiumTier",
1338 | "PremiumType",
1339 | "PresenceStatus",
1340 | "SortOrderTypes",
1341 | "StagePrivacyLevel",
1342 | "StickerFormatType",
1343 | "StickerType",
1344 | "SystemChannelFlags",
1345 | "TextInputStyle",
1346 | "TimeStampStyle",
1347 | "TriggerType",
1348 | "UpdatePresenceStatusType",
1349 | "UserFlags",
1350 | "VerificationLevel",
1351 | "VideoQualityMode",
1352 | "VisibilityType",
1353 | "WebhookType",
1354 | ]
1355 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/api/utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import Any, Literal, Union
3 |
4 | from nonebot.compat import type_validate_python
5 |
6 | from .model import (
7 | ExecuteWebhookParams,
8 | InteractionCallbackMessage,
9 | InteractionResponse,
10 | MessageSend,
11 | )
12 | from ..utils import model_dump
13 |
14 |
15 | def parse_data(
16 | data: dict[str, Any], model_class: type[Union[MessageSend, ExecuteWebhookParams]]
17 | ) -> dict[Literal["files", "json"], Any]:
18 | model = type_validate_python(model_class, data)
19 | payload: dict[str, Any] = model_dump(model, exclude={"files"}, exclude_none=True)
20 | if model.files:
21 | multipart: dict[str, Any] = {}
22 | attachments: list[dict] = payload.pop("attachments", [])
23 | for index, file in enumerate(model.files):
24 | for attachment in attachments:
25 | if attachment["filename"] == file.filename:
26 | attachment["id"] = index
27 | break
28 | multipart[f"files[{index}]"] = (file.filename, file.content)
29 | if attachments:
30 | payload["attachments"] = attachments
31 | multipart["payload_json"] = (None, json.dumps(payload), "application/json")
32 | return {"files": multipart}
33 | else:
34 | return {"json": payload}
35 |
36 |
37 | def parse_forum_thread_message(
38 | data: dict[str, Any],
39 | ) -> dict[Literal["files", "json"], Any]:
40 | model = type_validate_python(MessageSend, data)
41 | payload: dict[str, Any] = {}
42 | content: dict[str, Any] = model_dump(model, exclude={"files"}, exclude_none=True)
43 | if auto_archive_duration := data.pop("auto_archive_duration"):
44 | payload["auto_archive_duration"] = auto_archive_duration
45 | if rate_limit_per_user := data.pop("rate_limit_per_user"):
46 | payload["rate_limit_per_user"] = rate_limit_per_user
47 | if applied_tags := data.pop("applied_tags"):
48 | payload["applied_tags"] = applied_tags
49 | payload["message"] = content
50 | if model.files:
51 | multipart: dict[str, Any] = {"payload_json": None}
52 | attachments: list[dict] = payload.pop("attachments", [])
53 | for index, file in enumerate(model.files):
54 | for attachment in attachments:
55 | if attachment["filename"] == file.filename:
56 | attachment["id"] = str(index)
57 | break
58 | multipart[f"file[{index}]"] = (file.filename, file.content)
59 | if attachments:
60 | payload["attachments"] = attachments
61 | multipart["payload_json"] = (None, json.dumps(payload), "application/json")
62 | return {"files": multipart}
63 | else:
64 | return {"json": payload}
65 |
66 |
67 | def parse_interaction_response(
68 | response: InteractionResponse,
69 | ) -> dict[Literal["files", "json"], Any]:
70 | payload: dict[str, Any] = model_dump(response, exclude_none=True)
71 | if response.data and isinstance(response.data, InteractionCallbackMessage):
72 | payload["data"] = model_dump(
73 | response.data, exclude={"files"}, exclude_none=True
74 | )
75 | if response.data.files:
76 | multipart: dict[str, Any] = {}
77 | attachments: list[dict] = payload["data"].pop("attachments", [])
78 | for index, file in enumerate(response.data.files):
79 | for attachment in attachments:
80 | if attachment["filename"] == file.filename:
81 | attachment["id"] = index
82 | break
83 | multipart[f"files[{index}]"] = (file.filename, file.content)
84 | if attachments:
85 | payload["data"]["attachments"] = attachments
86 | multipart["payload_json"] = (
87 | None,
88 | json.dumps(payload),
89 | "application/json",
90 | )
91 | return {"files": multipart}
92 | return {"json": payload}
93 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/bot.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING, Any, Optional, Union
2 | from typing_extensions import override
3 |
4 | from nonebot.adapters import Bot as BaseBot
5 | from nonebot.message import handle_event
6 |
7 | from .api import (
8 | AllowedMention,
9 | ApiClient,
10 | InteractionCallbackMessage,
11 | InteractionCallbackType,
12 | InteractionResponse,
13 | MessageGet,
14 | MessageReference,
15 | Snowflake,
16 | SnowflakeType,
17 | User,
18 | )
19 | from .config import BotInfo
20 | from .event import Event, InteractionCreateEvent, MessageEvent
21 | from .exception import ActionFailed
22 | from .message import Message, MessageSegment, parse_message
23 | from .utils import log
24 |
25 | if TYPE_CHECKING:
26 | from .adapter import Adapter
27 |
28 |
29 | async def _check_reply(bot: "Bot", event: MessageEvent) -> None:
30 | if not event.message_reference or not event.message_reference.message_id:
31 | return
32 | try:
33 | msg = await bot.get_channel_message(
34 | channel_id=event.channel_id, message_id=event.message_reference.message_id
35 | )
36 | event.reply = msg
37 | if msg.author.id == bot.self_info.id:
38 | event.to_me = True
39 | except Exception as e:
40 | log("WARNING", f"Error when getting message reply info: {repr(e)}", e)
41 |
42 |
43 | def _check_at_me(bot: "Bot", event: MessageEvent) -> None:
44 | if event.mentions is not None and bot.self_info.id in [
45 | user.id for user in event.mentions
46 | ]:
47 | event.to_me = True
48 |
49 | def _is_at_me_seg(segment: MessageSegment) -> bool:
50 | return (
51 | segment.type == "mention_user"
52 | and segment.data.get("user_id") == bot.self_info.id
53 | )
54 |
55 | message = event.get_message()
56 |
57 | # ensure message is not empty
58 | if not message:
59 | message.append(MessageSegment.text(""))
60 |
61 | deleted = False
62 | if _is_at_me_seg(message[0]):
63 | message.pop(0)
64 | deleted = True
65 | if message and message[0].type == "text":
66 | message[0].data["text"] = message[0].data["text"].lstrip("\xa0").lstrip()
67 | if not message[0].data["text"]:
68 | del message[0]
69 |
70 | if not deleted:
71 | # check the last segment
72 | i = -1
73 | last_msg_seg = message[i]
74 | if (
75 | last_msg_seg.type == "text"
76 | and not last_msg_seg.data["text"].strip()
77 | and len(message) >= 2
78 | ):
79 | i -= 1
80 | last_msg_seg = message[i]
81 |
82 | if _is_at_me_seg(last_msg_seg):
83 | deleted = True
84 | del message[i:]
85 |
86 | if not message:
87 | message.append(MessageSegment.text(""))
88 |
89 |
90 | class Bot(BaseBot, ApiClient):
91 | """
92 | Discord 协议 Bot 适配。
93 | """
94 |
95 | adapter: "Adapter"
96 |
97 | @override
98 | def __init__(self, adapter: "Adapter", self_id: str, bot_info: BotInfo):
99 | super().__init__(adapter, self_id)
100 | self.adapter = adapter
101 | self._bot_info: BotInfo = bot_info
102 | self._application_id: Snowflake = Snowflake(self_id)
103 | self._session_id: Optional[str] = None
104 | self._self_info: Optional[User] = None
105 | self._sequence: Optional[int] = None
106 |
107 | @override
108 | def __repr__(self) -> str:
109 | return f"Bot(type={self.type!r}, self_id={self.self_id!r})"
110 |
111 | @property
112 | def ready(self) -> bool:
113 | return self._session_id is not None
114 |
115 | @property
116 | def bot_info(self) -> BotInfo:
117 | return self._bot_info
118 |
119 | @property
120 | def application_id(self) -> Snowflake:
121 | return self._application_id
122 |
123 | @property
124 | def session_id(self) -> str:
125 | if self._session_id is None:
126 | raise RuntimeError(f"Bot {self.self_id} is not connected!")
127 | return self._session_id
128 |
129 | @session_id.setter
130 | def session_id(self, session_id: str) -> None:
131 | self._session_id = session_id
132 |
133 | @property
134 | def self_info(self) -> User:
135 | if self._self_info is None:
136 | raise RuntimeError(f"Bot {self.bot_info} is not connected!")
137 | return self._self_info
138 |
139 | @self_info.setter
140 | def self_info(self, self_info: User) -> None:
141 | self._self_info = self_info
142 |
143 | @property
144 | def has_sequence(self) -> bool:
145 | return self._sequence is not None
146 |
147 | @property
148 | def sequence(self) -> int:
149 | if self._sequence is None:
150 | raise RuntimeError(f"Bot {self.self_id} is not connected!")
151 | return self._sequence
152 |
153 | @sequence.setter
154 | def sequence(self, sequence: int) -> None:
155 | self._sequence = sequence
156 |
157 | def clear(self) -> None:
158 | self._session_id = None
159 | self._sequence = None
160 |
161 | async def handle_event(self, event: Event) -> None:
162 | if isinstance(event, MessageEvent):
163 | await _check_reply(self, event)
164 | _check_at_me(self, event)
165 | await handle_event(self, event)
166 |
167 | async def send_to(
168 | self,
169 | channel_id: SnowflakeType,
170 | message: Union[str, Message, MessageSegment],
171 | tts: bool = False,
172 | nonce: Union[int, str, None] = None,
173 | allowed_mentions: Optional[AllowedMention] = None,
174 | ):
175 | message_data = parse_message(message)
176 |
177 | return await self.create_message(
178 | channel_id=channel_id,
179 | nonce=nonce,
180 | tts=tts,
181 | allowed_mentions=allowed_mentions,
182 | **message_data,
183 | )
184 |
185 | @override
186 | async def send(
187 | self,
188 | event: Event,
189 | message: Union[str, Message, MessageSegment],
190 | tts: bool = False,
191 | nonce: Union[int, str, None] = None,
192 | allowed_mentions: Optional[AllowedMention] = None,
193 | mention_sender: Optional[bool] = None,
194 | at_sender: Optional[bool] = None,
195 | reply_message: bool = False,
196 | **params: Any,
197 | ) -> MessageGet:
198 | """send message.
199 |
200 | Args:
201 | event: Event Object
202 | message: message to send
203 | mention_sender: whether @ event subject
204 | reply_message: whether reply event message
205 | tts: whether send as a TTS message
206 | nonce: can be used to verify a message was sent
207 | allowed_mentions: allowed mentions for the message
208 | **params: other params
209 |
210 | Returns:
211 | message model
212 | """
213 | message = MessageSegment.text(message) if isinstance(message, str) else message
214 | if isinstance(event, InteractionCreateEvent):
215 | message_data = parse_message(message)
216 | response = InteractionResponse(
217 | type=InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
218 | data=InteractionCallbackMessage(
219 | tts=tts, allowed_mentions=allowed_mentions, **message_data
220 | ),
221 | )
222 | try:
223 | await self.create_interaction_response(
224 | interaction_id=event.id,
225 | interaction_token=event.token,
226 | response=response,
227 | )
228 | except ActionFailed:
229 | return await self.create_followup_message(
230 | application_id=event.application_id,
231 | interaction_token=event.token,
232 | **message_data,
233 | )
234 | return await self.get_origin_interaction_response(
235 | application_id=event.application_id,
236 | interaction_token=event.token,
237 | )
238 |
239 | if not isinstance(event, MessageEvent) or not event.channel_id or not event.id:
240 | raise RuntimeError("Event cannot be replied to!")
241 | message = message if isinstance(message, Message) else Message(message)
242 | if mention_sender or at_sender:
243 | message.insert(0, MessageSegment.mention_user(event.user_id))
244 | if reply_message:
245 | message += MessageSegment.reference(MessageReference(message_id=event.id))
246 |
247 | message_data = parse_message(message)
248 |
249 | return await self.create_message(
250 | channel_id=event.channel_id,
251 | nonce=nonce,
252 | tts=tts,
253 | allowed_mentions=allowed_mentions,
254 | **message_data,
255 | )
256 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/commands/__init__.py:
--------------------------------------------------------------------------------
1 | from .matcher import (
2 | ApplicationCommandMatcher as ApplicationCommandMatcher,
3 | on_message_command as on_message_command,
4 | on_slash_command as on_slash_command,
5 | on_user_command as on_user_command,
6 | )
7 | from .params import (
8 | CommandMessage as CommandMessage,
9 | CommandOption as CommandOption,
10 | CommandOptionType as CommandOptionType,
11 | CommandUser as CommandUser,
12 | )
13 | from .storage import sync_application_command as sync_application_command
14 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/commands/matcher.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Iterable
2 | from datetime import datetime, timedelta
3 | from typing import Any, Optional, Union
4 |
5 | from nonebot.adapters import MessageTemplate
6 | from nonebot.dependencies import Dependent
7 | from nonebot.internal.matcher import (
8 | Matcher,
9 | current_bot,
10 | current_event,
11 | current_matcher,
12 | )
13 | from nonebot.internal.params import (
14 | ArgParam,
15 | BotParam,
16 | DefaultParam,
17 | DependParam,
18 | Depends,
19 | EventParam,
20 | MatcherParam,
21 | StateParam,
22 | )
23 | from nonebot.permission import Permission
24 | from nonebot.plugin.on import get_matcher_module, get_matcher_plugin, store_matcher
25 | from nonebot.rule import Rule
26 | from nonebot.typing import T_Handler, T_PermissionChecker, T_RuleChecker, T_State
27 |
28 | from .params import OptionParam
29 | from .storage import (
30 | _application_command_storage,
31 | )
32 | from ..api import (
33 | AnyCommandOption,
34 | ApplicationCommandCreate,
35 | ApplicationCommandOptionType,
36 | ApplicationCommandType,
37 | InteractionCallbackType,
38 | InteractionResponse,
39 | MessageFlag,
40 | MessageGet,
41 | Snowflake,
42 | SnowflakeType,
43 | )
44 | from ..bot import Bot
45 | from ..event import ApplicationCommandInteractionEvent
46 | from ..message import Message, MessageSegment, parse_message
47 |
48 | type_str_mapping = {
49 | ApplicationCommandOptionType.USER: "users",
50 | ApplicationCommandOptionType.CHANNEL: "channels",
51 | ApplicationCommandOptionType.ROLE: "roles",
52 | ApplicationCommandOptionType.ATTACHMENT: "attachments",
53 | }
54 |
55 |
56 | class ApplicationCommandConfig(ApplicationCommandCreate):
57 | guild_ids: Optional[list[Snowflake]] = None
58 |
59 |
60 | # def _application_command_rule(event: ApplicationCommandInteractionEvent) -> bool:
61 | # application_command = _application_command_storage.get(event.data.name)
62 | # if not application_command or event.data.type != application_command.type:
63 | # return False
64 | # if not event.data.guild_id and application_command.guild_ids is None:
65 | # return True
66 | # if (
67 | # event.data.guild_id
68 | # and application_command.guild_ids
69 | # and event.data.guild_id in application_command.guild_ids
70 | # ):
71 | # return True
72 | # return False
73 |
74 |
75 | class ApplicationCommandMatcher(Matcher):
76 | application_command: ApplicationCommandConfig
77 |
78 | @classmethod
79 | async def send_deferred_response(cls) -> None:
80 | event = current_event.get()
81 | bot = current_bot.get()
82 | if not isinstance(event, ApplicationCommandInteractionEvent) or not isinstance(
83 | bot, Bot
84 | ):
85 | raise ValueError("Invalid event or bot")
86 | await bot.create_interaction_response(
87 | interaction_id=event.id,
88 | interaction_token=event.token,
89 | response=InteractionResponse(
90 | type=InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE
91 | ),
92 | )
93 |
94 | @classmethod
95 | async def send_response(
96 | cls, message: Union[str, Message, MessageSegment, MessageTemplate]
97 | ) -> None:
98 | return await cls.send(message)
99 |
100 | @classmethod
101 | async def get_response(cls) -> MessageGet:
102 | event = current_event.get()
103 | bot = current_bot.get()
104 | if not isinstance(event, ApplicationCommandInteractionEvent) or not isinstance(
105 | bot, Bot
106 | ):
107 | raise ValueError("Invalid event or bot")
108 | return await bot.get_origin_interaction_response(
109 | application_id=event.application_id,
110 | interaction_token=event.token,
111 | )
112 |
113 | @classmethod
114 | async def edit_response(
115 | cls,
116 | message: Union[str, Message, MessageSegment, MessageTemplate],
117 | ) -> None:
118 | event = current_event.get()
119 | bot = current_bot.get()
120 | state = current_matcher.get().state
121 | if not isinstance(event, ApplicationCommandInteractionEvent) or not isinstance(
122 | bot, Bot
123 | ):
124 | raise ValueError("Invalid event or bot")
125 | if isinstance(message, MessageTemplate):
126 | _message = message.format(**state)
127 | else:
128 | _message = message
129 | message_data = parse_message(_message)
130 | await bot.edit_origin_interaction_response(
131 | application_id=event.application_id,
132 | interaction_token=event.token,
133 | **message_data,
134 | )
135 |
136 | @classmethod
137 | async def delete_response(cls) -> None:
138 | event = current_event.get()
139 | bot = current_bot.get()
140 | if not isinstance(event, ApplicationCommandInteractionEvent) or not isinstance(
141 | bot, Bot
142 | ):
143 | raise ValueError("Invalid event or bot")
144 | await bot.delete_origin_interaction_response(
145 | application_id=event.application_id,
146 | interaction_token=event.token,
147 | )
148 |
149 | @classmethod
150 | async def send_followup_msg(
151 | cls,
152 | message: Union[str, Message, MessageSegment, MessageTemplate],
153 | flags: Optional[MessageFlag] = None,
154 | ) -> MessageGet:
155 | event = current_event.get()
156 | bot = current_bot.get()
157 | state = current_matcher.get().state
158 | if not isinstance(event, ApplicationCommandInteractionEvent) or not isinstance(
159 | bot, Bot
160 | ):
161 | raise ValueError("Invalid event or bot")
162 | if isinstance(message, MessageTemplate):
163 | _message = message.format(**state)
164 | else:
165 | _message = message
166 | message_data = parse_message(_message)
167 | if flags:
168 | message_data["flags"] = int(flags)
169 | return await bot.create_followup_message(
170 | application_id=event.application_id,
171 | interaction_token=event.token,
172 | **message_data,
173 | )
174 |
175 | @classmethod
176 | async def get_followup_msg(cls, message_id: SnowflakeType):
177 | event = current_event.get()
178 | bot = current_bot.get()
179 | if not isinstance(event, ApplicationCommandInteractionEvent) or not isinstance(
180 | bot, Bot
181 | ):
182 | raise ValueError("Invalid event or bot")
183 | return await bot.get_followup_message(
184 | application_id=event.application_id,
185 | interaction_token=event.token,
186 | message_id=message_id,
187 | )
188 |
189 | @classmethod
190 | async def edit_followup_msg(
191 | cls,
192 | message_id: SnowflakeType,
193 | message: Union[str, Message, MessageSegment, MessageTemplate],
194 | ) -> MessageGet:
195 | event = current_event.get()
196 | bot = current_bot.get()
197 | state = current_matcher.get().state
198 | if not isinstance(event, ApplicationCommandInteractionEvent) or not isinstance(
199 | bot, Bot
200 | ):
201 | raise ValueError("Invalid event or bot")
202 | if isinstance(message, MessageTemplate):
203 | _message = message.format(**state)
204 | else:
205 | _message = message
206 | message_data = parse_message(_message)
207 | return await bot.edit_followup_message(
208 | application_id=event.application_id,
209 | interaction_token=event.token,
210 | message_id=message_id,
211 | **message_data,
212 | )
213 |
214 | @classmethod
215 | async def delete_followup_msg(cls, message_id: SnowflakeType) -> None:
216 | event = current_event.get()
217 | bot = current_bot.get()
218 | if not isinstance(event, ApplicationCommandInteractionEvent) or not isinstance(
219 | bot, Bot
220 | ):
221 | raise ValueError("Invalid event or bot")
222 | await bot.delete_followup_message(
223 | application_id=event.application_id,
224 | interaction_token=event.token,
225 | message_id=message_id,
226 | )
227 |
228 |
229 | class SlashCommandMatcher(ApplicationCommandMatcher):
230 | HANDLER_PARAM_TYPES = (
231 | DependParam,
232 | BotParam,
233 | EventParam,
234 | StateParam,
235 | ArgParam,
236 | MatcherParam,
237 | DefaultParam,
238 | OptionParam,
239 | )
240 |
241 | @classmethod
242 | def handle_sub_command(
243 | cls, *commands: str, parameterless: Optional[Iterable[Any]] = None
244 | ):
245 | def _sub_command_rule(
246 | event: ApplicationCommandInteractionEvent, matcher: Matcher, state: T_State
247 | ):
248 | if commands and not event.data.options:
249 | matcher.skip()
250 | options = event.data.options
251 | for command in commands:
252 | if not options:
253 | matcher.skip()
254 | option = options[0]
255 | if option.name != command or options[0].type not in (
256 | ApplicationCommandOptionType.SUB_COMMAND_GROUP,
257 | ApplicationCommandOptionType.SUB_COMMAND,
258 | ):
259 | matcher.skip()
260 | options = options[0].options if options[0].options else None
261 | # if options:
262 | # state[OPTION_KEY] = {}
263 | # for option in options:
264 | # if (
265 | # option.type
266 | # in (
267 | # ApplicationCommandOptionType.USER,
268 | # ApplicationCommandOptionType.CHANNEL,
269 | # ApplicationCommandOptionType.ROLE,
270 | # ApplicationCommandOptionType.ATTACHMENT,
271 | # )
272 | # and event.data.resolved
273 | # and (
274 | # data := getattr(
275 | # event.data.resolved, type_str_mapping[option.type]
276 | # )
277 | # )
278 | # ):
279 | # state[OPTION_KEY][option.name] = data[
280 | # Snowflake(option.value) # type: ignore
281 | # ]
282 | # elif (
283 | # option.type == ApplicationCommandOptionType.MENTIONABLE
284 | # and event.data.resolved
285 | # and event.data.resolved.users
286 | # ):
287 | # sid = Snowflake(option.value) # type: ignore
288 | # state[OPTION_KEY][option.name] = (
289 | # event.data.resolved.users.get(sid),
290 | # (
291 | # event.data.resolved.members.get(sid)
292 | # if event.data.resolved.members
293 | # else None
294 | # ),
295 | # )
296 | # elif option.type in (
297 | # ApplicationCommandOptionType.INTEGER,
298 | # ApplicationCommandOptionType.STRING,
299 | # ApplicationCommandOptionType.NUMBER,
300 | # ApplicationCommandOptionType.BOOLEAN,
301 | # ):
302 | # state[OPTION_KEY][option.name] = option.value
303 |
304 | parameterless = [Depends(_sub_command_rule), *(parameterless or [])]
305 |
306 | def _decorator(func: T_Handler) -> T_Handler:
307 | cls.append_handler(func, parameterless=parameterless)
308 | return func
309 |
310 | return _decorator
311 |
312 |
313 | class UserMessageCommandMatcher(ApplicationCommandMatcher):
314 | pass
315 |
316 |
317 | def on_slash_command(
318 | name: str,
319 | description: str,
320 | options: Optional[list[AnyCommandOption]] = None,
321 | internal_id: Optional[str] = None,
322 | rule: Union[Rule, T_RuleChecker, None] = None,
323 | permission: Union[Permission, T_PermissionChecker, None] = None,
324 | *,
325 | name_localizations: Optional[dict[str, str]] = None,
326 | description_localizations: Optional[dict[str, str]] = None,
327 | default_member_permissions: Optional[str] = None,
328 | dm_permission: Optional[bool] = None,
329 | default_permission: Optional[bool] = None,
330 | nsfw: Optional[bool] = None,
331 | handlers: Optional[list[Union[T_Handler, Dependent]]] = None,
332 | temp: bool = False,
333 | expire_time: Union[datetime, timedelta, None] = None,
334 | priority: int = 1,
335 | block: bool = True,
336 | state: Optional[T_State] = None,
337 | _depth: int = 0,
338 | ) -> type[SlashCommandMatcher]:
339 | config = ApplicationCommandConfig(
340 | type=ApplicationCommandType.CHAT_INPUT,
341 | name=name,
342 | name_localizations=name_localizations,
343 | description=description,
344 | description_localizations=description_localizations,
345 | options=options,
346 | default_member_permissions=default_member_permissions,
347 | dm_permission=dm_permission,
348 | default_permission=default_permission,
349 | nsfw=nsfw,
350 | )
351 | _application_command_storage[internal_id or name] = config
352 | matcher: type[SlashCommandMatcher] = SlashCommandMatcher.new(
353 | "notice",
354 | Rule() & rule,
355 | Permission() | permission,
356 | handlers=handlers,
357 | temp=temp,
358 | expire_time=expire_time,
359 | priority=priority,
360 | block=block,
361 | default_state=state,
362 | plugin=get_matcher_plugin(_depth + 1),
363 | module=get_matcher_module(_depth + 1),
364 | )
365 |
366 | def _application_command_rule(event: ApplicationCommandInteractionEvent) -> bool:
367 | if event.data.name != config.name or event.data.type != config.type:
368 | return False
369 | if not event.data.guild_id and config.guild_ids is None:
370 | return True
371 | if (
372 | event.data.guild_id
373 | and config.guild_ids
374 | and event.data.guild_id in config.guild_ids
375 | ):
376 | return True
377 | return False
378 |
379 | matcher.rule = matcher.rule & Rule(_application_command_rule)
380 |
381 | store_matcher(matcher)
382 | matcher.application_command = config
383 | return matcher
384 |
385 |
386 | def on_user_command(
387 | name: str,
388 | internal_id: Optional[str] = None,
389 | rule: Union[Rule, T_RuleChecker, None] = None,
390 | permission: Union[Permission, T_PermissionChecker, None] = None,
391 | *,
392 | name_localizations: Optional[dict[str, str]] = None,
393 | default_member_permissions: Optional[str] = None,
394 | dm_permission: Optional[bool] = None,
395 | default_permission: Optional[bool] = None,
396 | nsfw: Optional[bool] = None,
397 | handlers: Optional[list[Union[T_Handler, Dependent]]] = None,
398 | temp: bool = False,
399 | expire_time: Union[datetime, timedelta, None] = None,
400 | priority: int = 1,
401 | block: bool = True,
402 | state: Optional[T_State] = None,
403 | _depth: int = 0,
404 | ) -> type[UserMessageCommandMatcher]:
405 | config = ApplicationCommandConfig(
406 | type=ApplicationCommandType.USER,
407 | name=name,
408 | name_localizations=name_localizations,
409 | default_member_permissions=default_member_permissions,
410 | dm_permission=dm_permission,
411 | default_permission=default_permission,
412 | nsfw=nsfw,
413 | )
414 | _application_command_storage[internal_id or name] = config
415 | matcher: type[UserMessageCommandMatcher] = UserMessageCommandMatcher.new(
416 | "notice",
417 | Rule() & rule,
418 | Permission() | permission,
419 | handlers=handlers,
420 | temp=temp,
421 | expire_time=expire_time,
422 | priority=priority,
423 | block=block,
424 | default_state=state,
425 | plugin=get_matcher_plugin(_depth + 1),
426 | module=get_matcher_module(_depth + 1),
427 | )
428 |
429 | def _application_command_rule(event: ApplicationCommandInteractionEvent) -> bool:
430 | if event.data.name != config.name or event.data.type != config.type:
431 | return False
432 | if not event.data.guild_id and config.guild_ids is None:
433 | return True
434 | if (
435 | event.data.guild_id
436 | and config.guild_ids
437 | and event.data.guild_id in config.guild_ids
438 | ):
439 | return True
440 | return False
441 |
442 | matcher.rule = matcher.rule & Rule(_application_command_rule)
443 |
444 | store_matcher(matcher)
445 | matcher.application_command = config
446 | return matcher
447 |
448 |
449 | def on_message_command(
450 | name: str,
451 | internal_id: Optional[str] = None,
452 | rule: Union[Rule, T_RuleChecker, None] = None,
453 | permission: Union[Permission, T_PermissionChecker, None] = None,
454 | *,
455 | name_localizations: Optional[dict[str, str]] = None,
456 | default_member_permissions: Optional[str] = None,
457 | dm_permission: Optional[bool] = None,
458 | default_permission: Optional[bool] = None,
459 | nsfw: Optional[bool] = None,
460 | handlers: Optional[list[Union[T_Handler, Dependent]]] = None,
461 | temp: bool = False,
462 | expire_time: Union[datetime, timedelta, None] = None,
463 | priority: int = 1,
464 | block: bool = True,
465 | state: Optional[T_State] = None,
466 | _depth: int = 0,
467 | ) -> type[UserMessageCommandMatcher]:
468 | config = ApplicationCommandConfig(
469 | type=ApplicationCommandType.MESSAGE,
470 | name=name,
471 | name_localizations=name_localizations,
472 | default_member_permissions=default_member_permissions,
473 | dm_permission=dm_permission,
474 | default_permission=default_permission,
475 | nsfw=nsfw,
476 | )
477 | _application_command_storage[internal_id or name] = config
478 | matcher: type[UserMessageCommandMatcher] = UserMessageCommandMatcher.new(
479 | "notice",
480 | Rule() & rule,
481 | Permission() | permission,
482 | handlers=handlers,
483 | temp=temp,
484 | expire_time=expire_time,
485 | priority=priority,
486 | block=block,
487 | default_state=state,
488 | plugin=get_matcher_plugin(_depth + 1),
489 | module=get_matcher_module(_depth + 1),
490 | )
491 |
492 | def _application_command_rule(event: ApplicationCommandInteractionEvent) -> bool:
493 | if event.data.name != config.name or event.data.type != config.type:
494 | return False
495 | if not event.data.guild_id and config.guild_ids is None:
496 | return True
497 | if (
498 | event.data.guild_id
499 | and config.guild_ids
500 | and event.data.guild_id in config.guild_ids
501 | ):
502 | return True
503 | return False
504 |
505 | matcher.rule = matcher.rule & Rule(_application_command_rule)
506 |
507 | store_matcher(matcher)
508 | matcher.application_command = config
509 | return matcher
510 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/commands/params.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | from typing import Annotated, Any, Optional, TypeVar
3 | from typing_extensions import get_args, get_origin, override
4 |
5 | from nonebot.dependencies import Param
6 | from nonebot.params import Depends
7 |
8 | from ..api import (
9 | ApplicationCommandOptionType,
10 | ApplicationCommandType,
11 | MessageGet,
12 | Snowflake,
13 | User,
14 | )
15 | from ..event import ApplicationCommandInteractionEvent
16 |
17 | T = TypeVar("T")
18 |
19 | type_str_mapping = {
20 | ApplicationCommandOptionType.USER: "users",
21 | ApplicationCommandOptionType.CHANNEL: "channels",
22 | ApplicationCommandOptionType.ROLE: "roles",
23 | ApplicationCommandOptionType.ATTACHMENT: "attachments",
24 | }
25 |
26 |
27 | class CommandOptionType:
28 | def __init__(self, key: Optional[str] = None) -> None:
29 | self.key = key
30 |
31 | def __repr__(self) -> str:
32 | return f"ACommandOption(key={self.key!r})"
33 |
34 |
35 | class OptionParam(Param):
36 | def __init__(self, *args, key: str, **kwargs: Any) -> None:
37 | super().__init__(*args, **kwargs)
38 | self.key = key
39 |
40 | def __repr__(self) -> str:
41 | return f"OptionParam(key={self.key!r})"
42 |
43 | @classmethod
44 | @override
45 | def _check_param(
46 | cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
47 | ) -> Optional["OptionParam"]:
48 | if isinstance(param.default, CommandOptionType):
49 | return cls(key=param.default.key or param.name, validate=True)
50 | elif get_origin(param.annotation) is Annotated:
51 | for arg in get_args(param.annotation):
52 | if isinstance(arg, CommandOptionType):
53 | return cls(key=arg.key or param.name, validate=True)
54 |
55 | @override
56 | async def _solve(
57 | self, event: ApplicationCommandInteractionEvent, **kwargs: Any
58 | ) -> Any:
59 | if event.data.options:
60 | options = event.data.options
61 | if (
62 | options
63 | and options[0].type == ApplicationCommandOptionType.SUB_COMMAND_GROUP
64 | ):
65 | options = options[0].options
66 | if options and options[0].type == ApplicationCommandOptionType.SUB_COMMAND:
67 | options = options[0].options
68 | if options:
69 | for option in options:
70 | if option.name == self.key:
71 | if (
72 | option.type
73 | in (
74 | ApplicationCommandOptionType.USER,
75 | ApplicationCommandOptionType.CHANNEL,
76 | ApplicationCommandOptionType.ROLE,
77 | ApplicationCommandOptionType.ATTACHMENT,
78 | )
79 | and event.data.resolved
80 | and (
81 | data := getattr(
82 | event.data.resolved, type_str_mapping[option.type]
83 | )
84 | )
85 | ):
86 | return data[Snowflake(option.value)] # type: ignore
87 | elif (
88 | option.type == ApplicationCommandOptionType.MENTIONABLE
89 | and event.data.resolved
90 | and event.data.resolved.users
91 | ):
92 | sid = Snowflake(option.value) # type: ignore
93 | return (
94 | event.data.resolved.users.get(sid),
95 | (
96 | event.data.resolved.members.get(sid)
97 | if event.data.resolved.members
98 | else None
99 | ),
100 | )
101 | elif option.type in (
102 | ApplicationCommandOptionType.INTEGER,
103 | ApplicationCommandOptionType.STRING,
104 | ApplicationCommandOptionType.NUMBER,
105 | ApplicationCommandOptionType.BOOLEAN,
106 | ):
107 | return option.value
108 | return None
109 |
110 |
111 | def get_command_message(event: ApplicationCommandInteractionEvent):
112 | if (
113 | event.data.type == ApplicationCommandType.MESSAGE
114 | and event.data.target_id
115 | and event.data.resolved
116 | and event.data.resolved.messages
117 | ):
118 | return event.data.resolved.messages.get(event.data.target_id)
119 |
120 |
121 | def get_command_user(event: ApplicationCommandInteractionEvent):
122 | if (
123 | event.data.type == ApplicationCommandType.USER
124 | and event.data.target_id
125 | and event.data.resolved
126 | and event.data.resolved.users
127 | ):
128 | return event.data.resolved.users.get(event.data.target_id)
129 |
130 |
131 | CommandOption = Annotated[T, CommandOptionType()]
132 |
133 |
134 | CommandMessage = Annotated[MessageGet, Depends(get_command_message)]
135 | CommandUser = Annotated[User, Depends(get_command_user)]
136 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/commands/storage.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 | from typing import TYPE_CHECKING, Literal
3 |
4 | from ..api import ApplicationCommandCreate, Snowflake
5 | from ..bot import Bot
6 | from ..utils import model_dump
7 |
8 | if TYPE_CHECKING:
9 | from .matcher import ApplicationCommandConfig
10 |
11 | _application_command_storage: dict[str, "ApplicationCommandConfig"] = {}
12 |
13 | OPTION_KEY: Literal["_discord_application_command_options"] = (
14 | "_discord_application_command_options"
15 | )
16 |
17 |
18 | async def sync_application_command(bot: Bot):
19 | commands_global: list[ApplicationCommandCreate] = []
20 | commands_guild: dict[Snowflake, list[ApplicationCommandCreate]] = defaultdict(list)
21 | if "*" in bot.bot_info.application_commands:
22 | if "*" in bot.bot_info.application_commands["*"]:
23 | commands_global = [
24 | ApplicationCommandCreate(
25 | **model_dump(a, exclude={"guild_ids"}, exclude_none=True)
26 | )
27 | for a in _application_command_storage.values()
28 | ]
29 | else:
30 | for command in _application_command_storage.values():
31 | command.guild_ids = [
32 | g for g in bot.bot_info.application_commands["*"] if g != "*"
33 | ]
34 | for guild in command.guild_ids:
35 | commands_guild[guild].append(
36 | ApplicationCommandCreate(
37 | **model_dump(
38 | command, exclude={"guild_ids"}, exclude_none=True
39 | )
40 | )
41 | )
42 | else:
43 | for name, config in bot.bot_info.application_commands.items():
44 | command = _application_command_storage.get(name)
45 | if command:
46 | if "*" in config:
47 | commands_global.append(
48 | ApplicationCommandCreate(
49 | **model_dump(
50 | command, exclude={"guild_ids"}, exclude_none=True
51 | )
52 | )
53 | )
54 | else:
55 | command.guild_ids = [g for g in config if g != "*"]
56 | for guild in command.guild_ids:
57 | commands_guild[guild].append(
58 | ApplicationCommandCreate(
59 | **model_dump(
60 | command, exclude={"guild_ids"}, exclude_none=True
61 | )
62 | )
63 | )
64 | if commands_global:
65 | await bot.bulk_overwrite_global_application_commands(
66 | application_id=bot.application_id, commands=commands_global
67 | )
68 | for guild, commands in commands_guild.items():
69 | if commands:
70 | await bot.bulk_overwrite_guild_application_commands(
71 | application_id=bot.application_id,
72 | guild_id=guild,
73 | commands=commands,
74 | )
75 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/config.py:
--------------------------------------------------------------------------------
1 | from typing import Literal, Optional, Union
2 |
3 | from pydantic import BaseModel, Field
4 |
5 | from .api import Snowflake
6 |
7 |
8 | class Intents(BaseModel):
9 | guilds: bool = True
10 | guild_members: bool = False
11 | guild_moderation: bool = True
12 | guild_emojis_and_stickers: bool = True
13 | guild_integrations: bool = True
14 | guild_webhooks: bool = True
15 | guild_invites: bool = True
16 | guild_voice_states: bool = True
17 | guild_presences: bool = False
18 | guild_messages: bool = True
19 | guild_message_reactions: bool = True
20 | guild_message_typing: bool = True
21 | direct_messages: bool = True
22 | direct_message_reactions: bool = True
23 | direct_message_typing: bool = True
24 | message_content: bool = False
25 | guild_scheduled_events: bool = True
26 | auto_moderation_configuration: bool = True
27 | auto_moderation_execution: bool = True
28 |
29 | def to_int(self):
30 | return (
31 | self.guilds << 0
32 | | self.guild_members << 1
33 | | self.guild_moderation << 2
34 | | self.guild_emojis_and_stickers << 3
35 | | self.guild_integrations << 4
36 | | self.guild_webhooks << 5
37 | | self.guild_invites << 6
38 | | self.guild_voice_states << 7
39 | | self.guild_presences << 8
40 | | self.guild_messages << 9
41 | | self.guild_message_reactions << 10
42 | | self.guild_message_typing << 11
43 | | self.direct_messages << 12
44 | | self.direct_message_reactions << 13
45 | | self.direct_message_typing << 14
46 | | self.message_content << 15
47 | | self.guild_scheduled_events << 16
48 | | self.auto_moderation_configuration << 20
49 | | self.auto_moderation_execution << 21
50 | )
51 |
52 |
53 | class BotInfo(BaseModel):
54 | token: str
55 | shard: Optional[tuple[int, int]] = None
56 | intent: Intents = Field(default_factory=Intents)
57 | application_commands: dict[str, list[Union[Literal["*"], Snowflake]]] = Field(
58 | default_factory=dict
59 | )
60 |
61 |
62 | class Config(BaseModel):
63 | discord_bots: list[BotInfo] = Field(default_factory=list)
64 | discord_compress: bool = False
65 | discord_api_version: int = 10
66 | discord_api_timeout: float = 30.0
67 | discord_handle_self_message: bool = False
68 | discord_proxy: Optional[str] = None
69 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/event.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from enum import Enum
3 | from typing import Literal, Optional, Union
4 | from typing_extensions import override
5 |
6 | from nonebot.adapters import Event as BaseEvent
7 | from nonebot.compat import model_dump
8 | from nonebot.utils import escape_tag
9 |
10 | from pydantic import Field
11 |
12 | from .api.model import *
13 | from .api.types import UNSET, InteractionType, Missing
14 | from .message import Message
15 |
16 |
17 | class EventType(str, Enum):
18 | """Event Type
19 |
20 | see https://discord.com/developers/docs/topics/gateway-events#receive-events"""
21 |
22 | # Init Event
23 | HELLO = "HELLO"
24 | READY = "READY"
25 | RESUMED = "RESUMED"
26 | RECONNECT = "RECONNECT"
27 | INVALID_SESSION = "INVALID_SESSION"
28 |
29 | # APPLICATION
30 | APPLICATION_COMMAND_PERMISSIONS_UPDATE = "APPLICATION_COMMAND_PERMISSIONS_UPDATE"
31 |
32 | # AUTO MODERATION
33 | AUTO_MODERATION_RULE_CREATE = "AUTO_MODERATION_RULE_CREATE"
34 | AUTO_MODERATION_RULE_UPDATE = "AUTO_MODERATION_RULE_UPDATE"
35 | AUTO_MODERATION_RULE_DELETE = "AUTO_MODERATION_RULE_DELETE"
36 | AUTO_MODERATION_ACTION_EXECUTION = "AUTO_MODERATION_ACTION_EXECUTION"
37 |
38 | # CHANNELS
39 | CHANNEL_CREATE = "CHANNEL_CREATE"
40 | CHANNEL_UPDATE = "CHANNEL_UPDATE"
41 | CHANNEL_DELETE = "CHANNEL_DELETE"
42 | CHANNEL_PINS_UPDATE = "CHANNEL_PINS_UPDATE"
43 |
44 | # THREADS
45 | THREAD_CREATE = "THREAD_CREATE"
46 | THREAD_UPDATE = "THREAD_UPDATE"
47 | THREAD_DELETE = "THREAD_DELETE"
48 | THREAD_LIST_SYNC = "THREAD_LIST_SYNC"
49 | THREAD_MEMBER_UPDATE = "THREAD_MEMBER_UPDATE"
50 | THREAD_MEMBERS_UPDATE = "THREAD_MEMBERS_UPDATE"
51 |
52 | # GUILDS
53 | GUILD_CREATE = "GUILD_CREATE"
54 | GUILD_UPDATE = "GUILD_UPDATE"
55 | GUILD_DELETE = "GUILD_DELETE"
56 | GUILD_AUDIT_LOG_ENTRY_CREATE = "GUILD_AUDIT_LOG_ENTRY_CREATE"
57 | GUILD_BAN_ADD = "GUILD_BAN_ADD"
58 | GUILD_BAN_REMOVE = "GUILD_BAN_REMOVE"
59 | GUILD_EMOJIS_UPDATE = "GUILD_EMOJIS_UPDATE"
60 | GUILD_STICKERS_UPDATE = "GUILD_STICKERS_UPDATE"
61 | GUILD_INTEGRATIONS_UPDATE = "GUILD_INTEGRATIONS_UPDATE"
62 |
63 | # GUILD_MEMBERS
64 | GUILD_MEMBER_ADD = "GUILD_MEMBER_ADD"
65 | GUILD_MEMBER_UPDATE = "GUILD_MEMBER_UPDATE"
66 | GUILD_MEMBER_REMOVE = "GUILD_MEMBER_REMOVE"
67 | GUILD_MEMBERS_CHUNK = "GUILD_MEMBERS_CHUNK"
68 |
69 | # GUILD_ROLE
70 | GUILD_ROLE_CREATE = "GUILD_ROLE_CREATE"
71 | GUILD_ROLE_UPDATE = "GUILD_ROLE_UPDATE"
72 | GUILD_ROLE_DELETE = "GUILD_ROLE_DELETE"
73 |
74 | # GUILD_SCHEDULED_EVENT
75 | GUILD_SCHEDULED_EVENT_CREATE = "GUILD_SCHEDULED_EVENT_CREATE"
76 | GUILD_SCHEDULED_EVENT_UPDATE = "GUILD_SCHEDULED_EVENT_UPDATE"
77 | GUILD_SCHEDULED_EVENT_DELETE = "GUILD_SCHEDULED_EVENT_DELETE"
78 | GUILD_SCHEDULED_EVENT_USER_ADD = "GUILD_SCHEDULED_EVENT_USER_ADD"
79 | GUILD_SCHEDULED_EVENT_USER_REMOVE = "GUILD_SCHEDULED_EVENT_USER_REMOVE"
80 |
81 | # INTEGRATION
82 | INTEGRATION_CREATE = "INTEGRATION_CREATE"
83 | INTEGRATION_UPDATE = "INTEGRATION_UPDATE"
84 | INTEGRATION_DELETE = "INTEGRATION_DELETE"
85 | INTERACTION_CREATE = "INTERACTION_CREATE"
86 |
87 | # INVITE
88 | INVITE_CREATE = "INVITE_CREATE"
89 | INVITE_DELETE = "INVITE_DELETE"
90 |
91 | # MESSAGE
92 | MESSAGE_CREATE = "MESSAGE_CREATE"
93 | MESSAGE_UPDATE = "MESSAGE_UPDATE"
94 | MESSAGE_DELETE = "MESSAGE_DELETE"
95 | MESSAGE_DELETE_BULK = "MESSAGE_DELETE_BULK"
96 |
97 | # MESSAGE_REACTION
98 | MESSAGE_REACTION_ADD = "MESSAGE_REACTION_ADD"
99 | MESSAGE_REACTION_REMOVE = "MESSAGE_REACTION_REMOVE"
100 | MESSAGE_REACTION_REMOVE_ALL = "MESSAGE_REACTION_REMOVE_ALL"
101 | MESSAGE_REACTION_REMOVE_EMOJI = "MESSAGE_REACTION_REMOVE_EMOJI"
102 |
103 | # PRESENCE
104 | PRESENCE_UPDATE = "PRESENCE_UPDATE"
105 |
106 | # STAGE_INSTANCE
107 | STAGE_INSTANCE_CREATE = "STAGE_INSTANCE_CREATE"
108 | STAGE_INSTANCE_UPDATE = "STAGE_INSTANCE_UPDATE"
109 | STAGE_INSTANCE_DELETE = "STAGE_INSTANCE_DELETE"
110 |
111 | # TYPING
112 | TYPING_START = "TYPING_START"
113 |
114 | # USER
115 | USER_UPDATE = "USER_UPDATE"
116 |
117 | # VOICE
118 | VOICE_STATE_UPDATE = "VOICE_STATE_UPDATE"
119 | VOICE_SERVER_UPDATE = "VOICE_SERVER_UPDATE"
120 |
121 | # WEBHOOKS
122 | WEBHOOKS_UPDATE = "WEBHOOKS_UPDATE"
123 |
124 |
125 | class Event(BaseEvent):
126 | """Event"""
127 |
128 | __type__: EventType
129 | timestamp: datetime = Field(default_factory=datetime.now)
130 |
131 | @property
132 | def time(self) -> datetime:
133 | return self.timestamp
134 |
135 | @override
136 | def get_event_name(self) -> str:
137 | return self.__class__.__name__
138 |
139 | @override
140 | def get_event_description(self) -> str:
141 | return escape_tag(str(model_dump(self)))
142 |
143 | @override
144 | def get_message(self) -> Message:
145 | raise ValueError("Event has no message!")
146 |
147 | @override
148 | def get_user_id(self) -> str:
149 | raise ValueError("Event has no context!")
150 |
151 | @override
152 | def get_session_id(self) -> str:
153 | raise ValueError("Event has no context!")
154 |
155 | @override
156 | def is_tome(self) -> bool:
157 | return False
158 |
159 |
160 | class MetaEvent(Event):
161 | """Meta event"""
162 |
163 | @override
164 | def get_type(self) -> str:
165 | return "meta_event"
166 |
167 |
168 | class NoticeEvent(Event):
169 | """Notice event"""
170 |
171 | @override
172 | def get_type(self) -> str:
173 | return "notice"
174 |
175 |
176 | class RequestEvent(Event):
177 | """Request event"""
178 |
179 | @override
180 | def get_type(self) -> str:
181 | return "request"
182 |
183 |
184 | class MessageEvent(Event, MessageGet):
185 | """Message event"""
186 |
187 | to_me: bool = False
188 |
189 | reply: Optional[MessageGet] = None
190 |
191 | @property
192 | def message(self) -> Message:
193 | return self.get_message()
194 |
195 | @property
196 | def original_message(self) -> Message:
197 | return getattr(self, "_original_message", self.get_message()) # type: ignore
198 |
199 | @override
200 | def get_type(self) -> str:
201 | return "message"
202 |
203 | @override
204 | def get_user_id(self) -> str:
205 | return str(self.author.id)
206 |
207 | @override
208 | def get_session_id(self) -> str:
209 | return str(self.author.id)
210 |
211 | @override
212 | def get_message(self) -> Message:
213 | if not hasattr(self, "_message"):
214 | setattr(self, "_message", Message.from_guild_message(self))
215 | setattr(self, "_original_message", Message.from_guild_message(self))
216 | return getattr(self, "_message")
217 |
218 | @override
219 | def is_tome(self) -> bool:
220 | return self.to_me
221 |
222 | @property
223 | def user_id(self) -> Snowflake:
224 | return self.author.id
225 |
226 | @property
227 | def message_id(self) -> Snowflake:
228 | return self.id
229 |
230 |
231 | class HelloEvent(MetaEvent):
232 | """Hello event
233 |
234 | see https://discord.com/developers/docs/topics/gateway#hello"""
235 |
236 | __type__ = EventType.HELLO
237 |
238 | heartbeat_interval: int
239 |
240 |
241 | class ReadyEvent(MetaEvent, Ready):
242 | """Ready event
243 |
244 | see https://discord.com/developers/docs/topics/gateway-events#ready"""
245 |
246 | __type__ = EventType.READY
247 |
248 |
249 | class ResumedEvent(MetaEvent):
250 | """Resumed event
251 |
252 | see https://discord.com/developers/docs/topics/gateway-events#resumed"""
253 |
254 | __type__ = EventType.RESUMED
255 |
256 |
257 | class ReconnectEvent(MetaEvent):
258 | """Reconnect event
259 |
260 | see https://discord.com/developers/docs/topics/gateway-events#reconnect"""
261 |
262 | __type__ = EventType.RECONNECT
263 |
264 |
265 | class InvalidSessionEvent(MetaEvent):
266 | """Invalid session event
267 |
268 | see https://discord.com/developers/docs/topics/gateway-events#invalid-session"""
269 |
270 | __type__ = EventType.INVALID_SESSION
271 |
272 |
273 | class ApplicationCommandPermissionsUpdateEvent(NoticeEvent):
274 | """Application command create event
275 |
276 | see https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object
277 | """
278 |
279 | __type__ = EventType.APPLICATION_COMMAND_PERMISSIONS_UPDATE
280 | id: Snowflake
281 | application_id: Snowflake
282 | guild_id: Snowflake
283 | permissions: list[ApplicationCommandPermissions]
284 |
285 |
286 | class AutoModerationEvent(NoticeEvent):
287 | """Auto Moderation event"""
288 |
289 |
290 | class AutoModerationRuleCreateEvent(AutoModerationEvent, AutoModerationRuleCreate):
291 | """Automation update event
292 |
293 | see https://discord.com/developers/docs/topics/gateway-events#auto-moderation-rule-create
294 | """
295 |
296 | __type__ = EventType.AUTO_MODERATION_RULE_CREATE
297 |
298 |
299 | class AutoModerationRuleUpdateEvent(AutoModerationEvent, AutoModerationRuleUpdate):
300 | """Automation update event
301 |
302 | see https://discord.com/developers/docs/topics/gateway-events#auto-moderation-rule-update
303 | """
304 |
305 | __type__ = EventType.AUTO_MODERATION_RULE_UPDATE
306 |
307 |
308 | class AutoModerationRuleDeleteEvent(AutoModerationEvent, AutoModerationRuleDelete):
309 | """Automation update event
310 |
311 | see https://discord.com/developers/docs/topics/gateway-events#auto-moderation-rule-delete
312 | """
313 |
314 | __type__ = EventType.AUTO_MODERATION_RULE_DELETE
315 |
316 |
317 | class AutoModerationActionExecutionEvent(
318 | AutoModerationEvent, AutoModerationActionExecution
319 | ):
320 | """Automation update event
321 |
322 | see https://discord.com/developers/docs/topics/gateway-events#auto-moderation-action-execution
323 | """
324 |
325 | __type__ = EventType.AUTO_MODERATION_ACTION_EXECUTION
326 |
327 |
328 | class ChannelEvent(NoticeEvent):
329 | """Channel event
330 |
331 | see https://discord.com/developers/docs/topics/gateway-events#channels"""
332 |
333 |
334 | class ChannelCreateEvent(ChannelEvent, ChannelCreate):
335 | """Channel create event
336 |
337 | see https://discord.com/developers/docs/topics/gateway-events#channel-create"""
338 |
339 | __type__ = EventType.CHANNEL_CREATE
340 |
341 |
342 | class ChannelUpdateEvent(ChannelEvent, ChannelUpdate):
343 | """Channel update event
344 |
345 | see https://discord.com/developers/docs/topics/gateway-events#channel-update"""
346 |
347 | __type__ = EventType.CHANNEL_UPDATE
348 |
349 |
350 | class ChannelDeleteEvent(ChannelEvent, ChannelDelete):
351 | """Channel delete event
352 |
353 | see https://discord.com/developers/docs/topics/gateway-events#channel-delete"""
354 |
355 | __type__ = EventType.CHANNEL_DELETE
356 |
357 |
358 | class ChannelPinsUpdateEvent(ChannelEvent, ChannelPinsUpdate):
359 | """Channel pins update event
360 |
361 | see https://discord.com/developers/docs/topics/gateway-events#channel-pins-update"""
362 |
363 | __type__ = EventType.CHANNEL_PINS_UPDATE
364 |
365 |
366 | class ThreadEvent(NoticeEvent):
367 | """Thread event"""
368 |
369 |
370 | class ThreadCreateEvent(ThreadEvent, ThreadCreate):
371 | """Thread create event
372 |
373 | see https://discord.com/developers/docs/topics/gateway-events#thread-create"""
374 |
375 | __type__ = EventType.THREAD_CREATE
376 |
377 |
378 | class ThreadUpdateEvent(ThreadEvent, ThreadUpdate):
379 | """Thread update event
380 |
381 | see https://discord.com/developers/docs/topics/gateway-events#thread-update"""
382 |
383 | __type__ = EventType.THREAD_UPDATE
384 |
385 |
386 | class ThreadDeleteEvent(ThreadEvent, ThreadDelete):
387 | """Thread delete event
388 |
389 | see https://discord.com/developers/docs/topics/gateway-events#thread-delete"""
390 |
391 | __type__ = EventType.THREAD_DELETE
392 |
393 |
394 | class ThreadListSyncEvent(ThreadEvent, ThreadListSync):
395 | """Thread list sync event
396 |
397 | see https://discord.com/developers/docs/topics/gateway-events#thread-list-sync"""
398 |
399 | __type__ = EventType.THREAD_LIST_SYNC
400 |
401 |
402 | class ThreadMemberUpdateEvent(ThreadEvent, ThreadMemberUpdate):
403 | __type__ = EventType.THREAD_MEMBER_UPDATE
404 |
405 |
406 | class ThreadMembersUpdateEvent(ThreadEvent, ThreadMembersUpdate):
407 | __type__ = EventType.THREAD_MEMBERS_UPDATE
408 |
409 |
410 | class GuildEvent(NoticeEvent):
411 | """Guild event
412 |
413 | see https://discord.com/developers/docs/topics/gateway-events#guilds"""
414 |
415 |
416 | class GuildCreateEvent(GuildEvent, GuildCreate):
417 | """Guild create event
418 |
419 | see https://discord.com/developers/docs/topics/gateway-events#guild-create"""
420 |
421 | __type__ = EventType.GUILD_CREATE
422 |
423 |
424 | class GuildUpdateEvent(GuildEvent, GuildUpdate):
425 | """Guild update event
426 |
427 | see https://discord.com/developers/docs/topics/gateway-events#guild-update"""
428 |
429 | __type__ = EventType.GUILD_UPDATE
430 |
431 |
432 | class GuildDeleteEvent(GuildEvent, GuildDelete):
433 | """Guild delete event
434 |
435 | see https://discord.com/developers/docs/topics/gateway-events#guild-delete"""
436 |
437 | __type__ = EventType.GUILD_DELETE
438 |
439 |
440 | class GuildAuditLogEntryCreateEvent(GuildEvent, GuildAuditLogEntryCreate):
441 | """Guild audit log entry create event
442 |
443 | see https://discord.com/developers/docs/topics/gateway-events#guild-audit-log-entry-create
444 | """
445 |
446 | __type__ = EventType.GUILD_AUDIT_LOG_ENTRY_CREATE
447 |
448 |
449 | class GuildBanAddEvent(GuildEvent, GuildBanAdd):
450 | """Guild ban add event
451 |
452 | see https://discord.com/developers/docs/topics/gateway-events#guild-ban-add"""
453 |
454 | __type__ = EventType.GUILD_BAN_ADD
455 |
456 |
457 | class GuildBanRemoveEvent(GuildEvent, GuildBanRemove):
458 | """Guild ban remove event
459 |
460 | see https://discord.com/developers/docs/topics/gateway-events#guild-ban-remove"""
461 |
462 | __type__ = EventType.GUILD_BAN_REMOVE
463 |
464 |
465 | class GuildEmojisUpdateEvent(GuildEvent, GuildEmojisUpdate):
466 | """Guild emojis update event
467 |
468 | see https://discord.com/developers/docs/topics/gateway-events#guild-emojis-update"""
469 |
470 | __type__ = EventType.GUILD_EMOJIS_UPDATE
471 |
472 |
473 | class GuildStickersUpdateEvent(GuildEvent, GuildStickersUpdate):
474 | """Guild stickers update event
475 |
476 | see https://discord.com/developers/docs/topics/gateway-events#guild-stickers-update
477 | """
478 |
479 | __type__ = EventType.GUILD_STICKERS_UPDATE
480 |
481 |
482 | class GuildIntegrationsUpdateEvent(GuildEvent, GuildIntegrationsUpdate):
483 | """Guild integrations update event
484 |
485 | see https://discord.com/developers/docs/topics/gateway-events#guild-integrations-update
486 | """
487 |
488 | __type__ = EventType.GUILD_INTEGRATIONS_UPDATE
489 |
490 |
491 | class GuildMemberAddEvent(GuildEvent, GuildMemberAdd):
492 | """Guild member add event
493 |
494 | see https://discord.com/developers/docs/topics/gateway-events#guild-member-add"""
495 |
496 | __type__ = EventType.GUILD_MEMBER_ADD
497 |
498 |
499 | class GuildMemberRemoveEvent(GuildEvent, GuildMemberRemove):
500 | """Guild member remove event
501 |
502 | see https://discord.com/developers/docs/topics/gateway-events#guild-member-remove"""
503 |
504 | __type__ = EventType.GUILD_MEMBER_REMOVE
505 |
506 |
507 | class GuildMemberUpdateEvent(GuildEvent, GuildMemberUpdate):
508 | """Guild member update event
509 |
510 | see https://discord.com/developers/docs/topics/gateway-events#guild-member-update"""
511 |
512 | __type__ = EventType.GUILD_MEMBER_UPDATE
513 |
514 |
515 | class GuildMembersChunkEvent(GuildEvent, GuildMembersChunk):
516 | """Guild members chunk event
517 |
518 | see https://discord.com/developers/docs/topics/gateway-events#guild-members-chunk"""
519 |
520 | __type__ = EventType.GUILD_MEMBERS_CHUNK
521 |
522 |
523 | class GuildRoleCreateEvent(GuildEvent, GuildRoleCreate):
524 | """Guild role create event
525 |
526 | see https://discord.com/developers/docs/topics/gateway-events#guild-role-create"""
527 |
528 | __type__ = EventType.GUILD_ROLE_CREATE
529 |
530 |
531 | class GuildRoleUpdateEvent(GuildEvent, GuildRoleUpdate):
532 | """Guild role update event
533 |
534 | see https://discord.com/developers/docs/topics/gateway-events#guild-role-update"""
535 |
536 | __type__ = EventType.GUILD_ROLE_UPDATE
537 |
538 |
539 | class GuildRoleDeleteEvent(GuildEvent, GuildRoleDelete):
540 | """Guild role delete event
541 |
542 | see https://discord.com/developers/docs/topics/gateway-events#guild-role-delete"""
543 |
544 | __type__ = EventType.GUILD_ROLE_DELETE
545 |
546 |
547 | class GuildScheduledEventCreateEvent(GuildEvent, GuildScheduledEventCreate):
548 | """Guild scheduled event create event
549 |
550 | see https://discord.com/developers/docs/topics/gateway-events#guild-scheduled-event-create
551 | """
552 |
553 | __type__ = EventType.GUILD_SCHEDULED_EVENT_CREATE
554 |
555 |
556 | class GuildScheduledEventUpdateEvent(GuildEvent, GuildScheduledEventUpdate):
557 | """Guild scheduled event update event
558 |
559 | see https://discord.com/developers/docs/topics/gateway-events#guild-scheduled-event-update
560 | """
561 |
562 | __type__ = EventType.GUILD_SCHEDULED_EVENT_UPDATE
563 |
564 |
565 | class GuildScheduledEventDeleteEvent(GuildEvent, GuildScheduledEventDelete):
566 | """Guild scheduled event delete event
567 |
568 | see https://discord.com/developers/docs/topics/gateway-events#guild-scheduled-event-delete
569 | """
570 |
571 | __type__ = EventType.GUILD_SCHEDULED_EVENT_DELETE
572 |
573 |
574 | class GuildScheduledEventUserAddEvent(GuildEvent, GuildScheduledEventUserAdd):
575 | """Guild scheduled event user add event
576 |
577 | see https://discord.com/developers/docs/topics/gateway-events#guild-scheduled-event-user-add
578 | """
579 |
580 | __type__ = EventType.GUILD_SCHEDULED_EVENT_USER_ADD
581 |
582 |
583 | class GuildScheduledEventUserRemoveEvent(GuildEvent, GuildScheduledEventUserRemove):
584 | """Guild scheduled event user remove event
585 |
586 | see https://discord.com/developers/docs/topics/gateway-events#guild-scheduled-event-user-remove
587 | """
588 |
589 | __type__ = EventType.GUILD_SCHEDULED_EVENT_USER_REMOVE
590 |
591 |
592 | class IntegrationEvent(NoticeEvent):
593 | """Integration event"""
594 |
595 |
596 | class IntegrationCreateEvent(IntegrationEvent, IntegrationCreate):
597 | """Integration create event
598 |
599 | see https://discord.com/developers/docs/topics/gateway-events#integration-create"""
600 |
601 | __type__ = EventType.INTEGRATION_CREATE
602 |
603 |
604 | class IntegrationUpdateEvent(IntegrationEvent, IntegrationUpdate):
605 | """Integration update event
606 |
607 | see https://discord.com/developers/docs/topics/gateway-events#integration-update"""
608 |
609 | __type__ = EventType.INTEGRATION_UPDATE
610 |
611 |
612 | class IntegrationDeleteEvent(IntegrationEvent, IntegrationDelete):
613 | """Integration delete event
614 |
615 | see https://discord.com/developers/docs/topics/gateway-events#integration-delete"""
616 |
617 | __type__ = EventType.INTEGRATION_DELETE
618 |
619 |
620 | class InteractionCreateEvent(NoticeEvent, InteractionCreate):
621 | """Interaction create event
622 |
623 | see https://discord.com/developers/docs/topics/gateway-events#interaction-create"""
624 |
625 | __type__ = EventType.INTERACTION_CREATE
626 |
627 | @override
628 | def get_user_id(self) -> str:
629 | if not self.user:
630 | raise ValueError("Event has no context!")
631 | return str(self.user.id)
632 |
633 |
634 | class PingInteractionEvent(InteractionCreateEvent):
635 | type: Literal[InteractionType.PING]
636 | data: Literal[UNSET] = UNSET
637 |
638 |
639 | class ApplicationCommandInteractionEvent(InteractionCreateEvent):
640 | type: Literal[InteractionType.APPLICATION_COMMAND]
641 | data: ApplicationCommandData
642 |
643 |
644 | class ApplicationCommandAutoCompleteInteractionEvent(InteractionCreateEvent):
645 | type: Literal[InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE]
646 | data: ApplicationCommandData
647 |
648 |
649 | class MessageComponentInteractionEvent(InteractionCreateEvent):
650 | type: Literal[InteractionType.MESSAGE_COMPONENT]
651 | data: MessageComponentData
652 | message: MessageGet
653 |
654 |
655 | class ModalSubmitInteractionEvent(InteractionCreateEvent):
656 | type: Literal[InteractionType.MODAL_SUBMIT]
657 | data: ModalSubmitData
658 |
659 |
660 | class InviteCreateEvent(NoticeEvent, InviteCreate):
661 | """Invite create event
662 |
663 | see https://discord.com/developers/docs/topics/gateway-events#invite-create"""
664 |
665 | __type__ = EventType.INVITE_CREATE
666 |
667 |
668 | class InviteDeleteEvent(NoticeEvent):
669 | """Invite delete event
670 |
671 | see https://discord.com/developers/docs/topics/gateway-events#invite-delete"""
672 |
673 | __type__ = EventType.INVITE_DELETE
674 | channel_id: Snowflake
675 | guild_id: Missing[Snowflake] = UNSET
676 | code: str
677 |
678 |
679 | class MessageCreateEvent(MessageEvent, MessageCreate):
680 | """Message Create Event
681 |
682 | see https://discord.com/developers/docs/topics/gateway-events#message-create
683 | """
684 |
685 | __type__ = EventType.MESSAGE_CREATE
686 |
687 |
688 | class GuildMessageCreateEvent(MessageCreateEvent):
689 | guild_id: Snowflake
690 |
691 |
692 | class DirectMessageCreateEvent(MessageCreateEvent):
693 | to_me: bool = True
694 | guild_id: Literal[UNSET] = Field(UNSET, exclude=True)
695 |
696 |
697 | class MessageUpdateEvent(NoticeEvent, MessageUpdate):
698 | """Message Update Event
699 |
700 | see https://discord.com/developers/docs/topics/gateway-events#message-update
701 | """
702 |
703 | __type__ = EventType.MESSAGE_UPDATE
704 |
705 |
706 | class GuildMessageUpdateEvent(MessageUpdateEvent):
707 | guild_id: Snowflake
708 |
709 |
710 | class DirectMessageUpdateEvent(MessageUpdateEvent):
711 | guild_id: Literal[UNSET] = Field(UNSET, exclude=True)
712 |
713 |
714 | class MessageDeleteEvent(NoticeEvent, MessageDelete):
715 | """Message Delete Event
716 |
717 | see https://discord.com/developers/docs/topics/gateway-events#message-delete
718 | """
719 |
720 | __type__ = EventType.MESSAGE_DELETE
721 |
722 |
723 | class GuildMessageDeleteEvent(MessageDeleteEvent):
724 | guild_id: Snowflake
725 |
726 |
727 | class DirectMessageDeleteEvent(MessageDeleteEvent):
728 | guild_id: Literal[UNSET] = Field(UNSET, exclude=True)
729 |
730 |
731 | class MessageDeleteBulkEvent(NoticeEvent, MessageDeleteBulk):
732 | """Message Delete Bulk Event
733 |
734 | see https://discord.com/developers/docs/topics/gateway-events#message-delete-bulk
735 | """
736 |
737 | __type__ = EventType.MESSAGE_DELETE
738 |
739 |
740 | class GuildMessageDeleteBulkEvent(MessageDeleteBulkEvent):
741 | guild_id: Snowflake
742 |
743 |
744 | class DirectMessageDeleteBulkEvent(MessageDeleteBulkEvent):
745 | guild_id: Literal[UNSET] = Field(UNSET, exclude=True)
746 |
747 |
748 | class MessageReactionAddEvent(NoticeEvent, MessageReactionAdd):
749 | """
750 | Message Reaction Add Event
751 |
752 | see https://discord.com/developers/docs/topics/gateway#message-reaction-add
753 | """
754 |
755 | __type__ = EventType.MESSAGE_REACTION_ADD
756 |
757 |
758 | class GuildMessageReactionAddEvent(MessageReactionAddEvent):
759 | guild_id: Snowflake
760 |
761 |
762 | class DirectMessageReactionAddEvent(MessageReactionAddEvent):
763 | guild_id: Literal[UNSET] = Field(UNSET, exclude=True)
764 |
765 |
766 | class MessageReactionRemoveEvent(NoticeEvent, MessageReactionRemove):
767 | """Message Reaction Remove Event
768 |
769 | see https://discord.com/developers/docs/topics/gateway-events#message-reaction-remove
770 | """
771 |
772 | __type__ = EventType.MESSAGE_REACTION_REMOVE
773 |
774 |
775 | class GuildMessageReactionRemoveEvent(MessageReactionRemoveEvent):
776 | guild_id: Snowflake
777 |
778 |
779 | class DirectMessageReactionRemoveEvent(MessageReactionRemoveEvent):
780 | guild_id: Literal[UNSET] = Field(UNSET, exclude=True)
781 |
782 |
783 | class MessageReactionRemoveAllEvent(NoticeEvent, MessageReactionRemoveAll):
784 | """Message Reaction Remove All Event
785 |
786 | see https://discord.com/developers/docs/topics/gateway-events#message-reaction-remove-all
787 | """
788 |
789 | __type__ = EventType.MESSAGE_REACTION_REMOVE_ALL
790 |
791 |
792 | class GuildMessageReactionRemoveAllEvent(MessageReactionRemoveAllEvent):
793 | guild_id: Snowflake
794 |
795 |
796 | class DirectMessageReactionRemoveAllEvent(MessageReactionRemoveAllEvent):
797 | guild_id: Literal[UNSET] = Field(UNSET, exclude=True)
798 |
799 |
800 | class MessageReactionRemoveEmojiEvent(NoticeEvent, MessageReactionRemoveEmoji):
801 | """Message Reaction Remove Emoji Event
802 |
803 | see https://discord.com/developers/docs/topics/gateway-events#message-reaction-remove-emoji
804 | """
805 |
806 | __type__ = EventType.MESSAGE_REACTION_REMOVE_EMOJI
807 |
808 |
809 | class GuildMessageReactionRemoveEmojiEvent(MessageReactionRemoveEmojiEvent):
810 | guild_id: Snowflake
811 |
812 |
813 | class DirectMessageReactionRemoveEmojiEvent(MessageReactionRemoveEmojiEvent):
814 | guild_id: Literal[UNSET] = Field(UNSET, exclude=True)
815 |
816 |
817 | class PresenceUpdateEvent(NoticeEvent, PresenceUpdate):
818 | """Presence Update Event
819 |
820 | see https://discord.com/developers/docs/topics/gateway-events#presence-update
821 | """
822 |
823 | __type__ = EventType.PRESENCE_UPDATE
824 |
825 |
826 | class StageInstanceCreateEvent(GuildEvent, StageInstanceCreate):
827 | """Stage instance create event
828 |
829 | see https://discord.com/developers/docs/topics/gateway-events#stage-instance-create
830 | """
831 |
832 | __type__ = EventType.STAGE_INSTANCE_CREATE
833 |
834 |
835 | class StageInstanceUpdateEvent(GuildEvent, StageInstanceUpdate):
836 | """Stage instance update event
837 |
838 | see https://discord.com/developers/docs/topics/gateway-events#stage-instance-update
839 | """
840 |
841 | __type__ = EventType.STAGE_INSTANCE_UPDATE
842 |
843 |
844 | class StageInstanceDeleteEvent(GuildEvent, StageInstanceDelete):
845 | """Stage instance delete event
846 |
847 | see https://discord.com/developers/docs/topics/gateway-events#stage-instance-delete
848 | """
849 |
850 | __type__ = EventType.STAGE_INSTANCE_DELETE
851 |
852 |
853 | class TypingStartEvent(NoticeEvent, TypingStart):
854 | """Typing Start Event
855 |
856 | see https://discord.com/developers/docs/topics/gateway-events#typing-start
857 | """
858 |
859 | __type__ = EventType.TYPING_START
860 |
861 |
862 | class GuildTypingStartEvent(TypingStartEvent):
863 | guild_id: Snowflake
864 | member: GuildMember
865 |
866 |
867 | class DirectTypingStartEvent(TypingStartEvent):
868 | guild_id: Literal[UNSET] = Field(UNSET, exclude=True)
869 | member: Literal[UNSET] = Field(UNSET, exclude=True)
870 |
871 |
872 | class UserUpdateEvent(NoticeEvent, UserUpdate):
873 | """User Update Event
874 |
875 | see https://discord.com/developers/docs/topics/gateway-events#user-update
876 | """
877 |
878 | __type__ = EventType.USER_UPDATE
879 |
880 |
881 | class VoiceStateUpdateEvent(NoticeEvent, VoiceStateUpdate):
882 | """Voice State Update Event
883 |
884 | see https://discord.com/developers/docs/topics/gateway-events#voice-state-update
885 | """
886 |
887 | __type__ = EventType.VOICE_STATE_UPDATE
888 |
889 |
890 | class VoiceServerUpdateEvent(NoticeEvent, VoiceServerUpdate):
891 | """Voice Server Update Event
892 |
893 | see https://discord.com/developers/docs/topics/gateway-events#voice-server-update
894 | """
895 |
896 | __type__ = EventType.VOICE_SERVER_UPDATE
897 |
898 |
899 | class WebhooksUpdateEvent(NoticeEvent, WebhooksUpdate):
900 | """Webhooks Update Event
901 |
902 | see https://discord.com/developers/docs/topics/gateway-events#webhooks-update
903 | """
904 |
905 | __type__ = EventType.WEBHOOKS_UPDATE
906 |
907 |
908 | event_classes: dict[str, type[Event]] = {
909 | EventType.HELLO.value: HelloEvent,
910 | EventType.READY.value: ReadyEvent,
911 | EventType.RESUMED.value: ResumedEvent,
912 | EventType.RECONNECT.value: ReconnectEvent,
913 | EventType.INVALID_SESSION.value: InvalidSessionEvent,
914 | EventType.APPLICATION_COMMAND_PERMISSIONS_UPDATE.value: (
915 | ApplicationCommandPermissionsUpdateEvent
916 | ),
917 | EventType.AUTO_MODERATION_RULE_CREATE.value: AutoModerationRuleCreateEvent,
918 | EventType.AUTO_MODERATION_RULE_UPDATE.value: AutoModerationRuleUpdateEvent,
919 | EventType.AUTO_MODERATION_RULE_DELETE.value: AutoModerationRuleDeleteEvent,
920 | EventType.AUTO_MODERATION_ACTION_EXECUTION.value: (
921 | AutoModerationActionExecutionEvent
922 | ),
923 | EventType.CHANNEL_CREATE.value: ChannelCreateEvent,
924 | EventType.CHANNEL_UPDATE.value: ChannelUpdateEvent,
925 | EventType.CHANNEL_DELETE.value: ChannelDeleteEvent,
926 | EventType.CHANNEL_PINS_UPDATE.value: ChannelPinsUpdateEvent,
927 | EventType.THREAD_CREATE.value: ThreadCreateEvent,
928 | EventType.THREAD_UPDATE.value: ThreadUpdateEvent,
929 | EventType.THREAD_DELETE.value: ThreadDeleteEvent,
930 | EventType.THREAD_LIST_SYNC.value: ThreadListSyncEvent,
931 | EventType.THREAD_MEMBER_UPDATE.value: ThreadMemberUpdateEvent,
932 | EventType.THREAD_MEMBERS_UPDATE.value: ThreadMembersUpdateEvent,
933 | EventType.GUILD_CREATE.value: GuildCreateEvent,
934 | EventType.GUILD_UPDATE.value: GuildUpdateEvent,
935 | EventType.GUILD_DELETE.value: GuildDeleteEvent,
936 | EventType.GUILD_AUDIT_LOG_ENTRY_CREATE.value: GuildAuditLogEntryCreateEvent,
937 | EventType.GUILD_BAN_ADD.value: GuildBanAddEvent,
938 | EventType.GUILD_BAN_REMOVE.value: GuildBanRemoveEvent,
939 | EventType.GUILD_EMOJIS_UPDATE.value: GuildEmojisUpdateEvent,
940 | EventType.GUILD_STICKERS_UPDATE.value: GuildStickersUpdateEvent,
941 | EventType.GUILD_INTEGRATIONS_UPDATE.value: GuildIntegrationsUpdateEvent,
942 | EventType.GUILD_MEMBER_ADD.value: GuildMemberAddEvent,
943 | EventType.GUILD_MEMBER_REMOVE.value: GuildMemberRemoveEvent,
944 | EventType.GUILD_MEMBER_UPDATE.value: GuildMemberUpdateEvent,
945 | EventType.GUILD_MEMBERS_CHUNK.value: GuildMembersChunkEvent,
946 | EventType.GUILD_ROLE_CREATE.value: GuildRoleCreateEvent,
947 | EventType.GUILD_ROLE_UPDATE.value: GuildRoleUpdateEvent,
948 | EventType.GUILD_ROLE_DELETE.value: GuildRoleDeleteEvent,
949 | EventType.GUILD_SCHEDULED_EVENT_CREATE.value: GuildScheduledEventCreateEvent,
950 | EventType.GUILD_SCHEDULED_EVENT_UPDATE.value: GuildScheduledEventUpdateEvent,
951 | EventType.GUILD_SCHEDULED_EVENT_DELETE.value: GuildScheduledEventDeleteEvent,
952 | EventType.GUILD_SCHEDULED_EVENT_USER_ADD.value: GuildScheduledEventUserAddEvent,
953 | EventType.GUILD_SCHEDULED_EVENT_USER_REMOVE.value: (
954 | GuildScheduledEventUserRemoveEvent
955 | ),
956 | EventType.INTEGRATION_CREATE.value: IntegrationCreateEvent,
957 | EventType.INTEGRATION_UPDATE.value: IntegrationUpdateEvent,
958 | EventType.INTEGRATION_DELETE.value: IntegrationDeleteEvent,
959 | EventType.INTERACTION_CREATE.value: Union[
960 | PingInteractionEvent,
961 | ApplicationCommandInteractionEvent,
962 | ApplicationCommandAutoCompleteInteractionEvent,
963 | MessageComponentInteractionEvent,
964 | ModalSubmitInteractionEvent,
965 | InteractionCreateEvent,
966 | ],
967 | EventType.INVITE_CREATE.value: InviteCreateEvent,
968 | EventType.INVITE_DELETE.value: InviteDeleteEvent,
969 | EventType.MESSAGE_CREATE.value: Union[
970 | GuildMessageCreateEvent, DirectMessageCreateEvent, MessageCreateEvent
971 | ],
972 | EventType.MESSAGE_UPDATE.value: Union[
973 | GuildMessageUpdateEvent, DirectMessageUpdateEvent, MessageUpdateEvent
974 | ],
975 | EventType.MESSAGE_DELETE.value: Union[
976 | GuildMessageDeleteEvent, DirectMessageDeleteEvent, MessageDeleteEvent
977 | ],
978 | EventType.MESSAGE_DELETE_BULK.value: Union[
979 | GuildMessageDeleteBulkEvent,
980 | DirectMessageDeleteBulkEvent,
981 | MessageDeleteBulkEvent,
982 | ],
983 | EventType.MESSAGE_REACTION_ADD.value: Union[
984 | GuildMessageReactionAddEvent,
985 | DirectMessageReactionAddEvent,
986 | MessageReactionAddEvent,
987 | ],
988 | EventType.MESSAGE_REACTION_REMOVE: Union[
989 | GuildMessageReactionRemoveEvent,
990 | DirectMessageReactionRemoveEvent,
991 | MessageReactionRemoveEvent,
992 | ],
993 | EventType.MESSAGE_REACTION_REMOVE_ALL: Union[
994 | GuildMessageReactionRemoveAllEvent,
995 | DirectMessageReactionRemoveAllEvent,
996 | MessageReactionRemoveAllEvent,
997 | ],
998 | EventType.MESSAGE_REACTION_REMOVE_EMOJI: Union[
999 | GuildMessageReactionRemoveEmojiEvent,
1000 | DirectMessageReactionRemoveEmojiEvent,
1001 | MessageReactionRemoveEmojiEvent,
1002 | ],
1003 | EventType.PRESENCE_UPDATE.value: PresenceUpdateEvent,
1004 | EventType.STAGE_INSTANCE_CREATE.value: StageInstanceCreateEvent,
1005 | EventType.STAGE_INSTANCE_UPDATE.value: StageInstanceUpdateEvent,
1006 | EventType.STAGE_INSTANCE_DELETE.value: StageInstanceDeleteEvent,
1007 | EventType.TYPING_START.value: Union[
1008 | GuildTypingStartEvent, DirectTypingStartEvent, TypingStartEvent
1009 | ],
1010 | EventType.USER_UPDATE.value: UserUpdateEvent,
1011 | EventType.VOICE_STATE_UPDATE.value: VoiceStateUpdateEvent,
1012 | EventType.VOICE_SERVER_UPDATE.value: VoiceServerUpdateEvent,
1013 | EventType.WEBHOOKS_UPDATE.value: WebhooksUpdateEvent,
1014 | } # type: ignore
1015 |
1016 | __all__ = [
1017 | "EventType",
1018 | "Event",
1019 | "MetaEvent",
1020 | "NoticeEvent",
1021 | "RequestEvent",
1022 | "MessageEvent",
1023 | "HelloEvent",
1024 | "ReadyEvent",
1025 | "ResumedEvent",
1026 | "ReconnectEvent",
1027 | "InvalidSessionEvent",
1028 | "ApplicationCommandPermissionsUpdateEvent",
1029 | "AutoModerationEvent",
1030 | "AutoModerationRuleCreateEvent",
1031 | "AutoModerationRuleUpdateEvent",
1032 | "AutoModerationRuleDeleteEvent",
1033 | "AutoModerationActionExecutionEvent",
1034 | "ChannelEvent",
1035 | "ChannelCreateEvent",
1036 | "ChannelUpdateEvent",
1037 | "ChannelDeleteEvent",
1038 | "ChannelPinsUpdateEvent",
1039 | "ThreadEvent",
1040 | "ThreadCreateEvent",
1041 | "ThreadUpdateEvent",
1042 | "ThreadDeleteEvent",
1043 | "ThreadListSyncEvent",
1044 | "ThreadMemberUpdateEvent",
1045 | "ThreadMembersUpdateEvent",
1046 | "GuildEvent",
1047 | "GuildCreateEvent",
1048 | "GuildUpdateEvent",
1049 | "GuildDeleteEvent",
1050 | "GuildAuditLogEntryCreateEvent",
1051 | "GuildBanAddEvent",
1052 | "GuildBanRemoveEvent",
1053 | "GuildEmojisUpdateEvent",
1054 | "GuildStickersUpdateEvent",
1055 | "GuildIntegrationsUpdateEvent",
1056 | "GuildMemberAddEvent",
1057 | "GuildMemberRemoveEvent",
1058 | "GuildMemberUpdateEvent",
1059 | "GuildMembersChunkEvent",
1060 | "GuildRoleCreateEvent",
1061 | "GuildRoleUpdateEvent",
1062 | "GuildRoleDeleteEvent",
1063 | "GuildScheduledEventCreateEvent",
1064 | "GuildScheduledEventUpdateEvent",
1065 | "GuildScheduledEventDeleteEvent",
1066 | "GuildScheduledEventUserAddEvent",
1067 | "GuildScheduledEventUserRemoveEvent",
1068 | "IntegrationEvent",
1069 | "IntegrationCreateEvent",
1070 | "IntegrationUpdateEvent",
1071 | "IntegrationDeleteEvent",
1072 | "InteractionCreateEvent",
1073 | "PingInteractionEvent",
1074 | "ApplicationCommandInteractionEvent",
1075 | "ApplicationCommandAutoCompleteInteractionEvent",
1076 | "MessageComponentInteractionEvent",
1077 | "ModalSubmitInteractionEvent",
1078 | "InviteCreateEvent",
1079 | "InviteDeleteEvent",
1080 | "MessageCreateEvent",
1081 | "GuildMessageCreateEvent",
1082 | "DirectMessageCreateEvent",
1083 | "MessageUpdateEvent",
1084 | "GuildMessageUpdateEvent",
1085 | "DirectMessageUpdateEvent",
1086 | "MessageDeleteEvent",
1087 | "GuildMessageDeleteEvent",
1088 | "DirectMessageDeleteEvent",
1089 | "MessageDeleteBulkEvent",
1090 | "GuildMessageDeleteBulkEvent",
1091 | "DirectMessageDeleteBulkEvent",
1092 | "MessageReactionAddEvent",
1093 | "GuildMessageReactionAddEvent",
1094 | "DirectMessageReactionAddEvent",
1095 | "MessageReactionRemoveEvent",
1096 | "GuildMessageReactionRemoveEvent",
1097 | "DirectMessageReactionRemoveEvent",
1098 | "MessageReactionRemoveAllEvent",
1099 | "GuildMessageReactionRemoveAllEvent",
1100 | "DirectMessageReactionRemoveAllEvent",
1101 | "MessageReactionRemoveEmojiEvent",
1102 | "GuildMessageReactionRemoveEmojiEvent",
1103 | "DirectMessageReactionRemoveEmojiEvent",
1104 | "PresenceUpdateEvent",
1105 | "StageInstanceCreateEvent",
1106 | "StageInstanceUpdateEvent",
1107 | "StageInstanceDeleteEvent",
1108 | "TypingStartEvent",
1109 | "GuildTypingStartEvent",
1110 | "DirectTypingStartEvent",
1111 | "UserUpdateEvent",
1112 | "VoiceStateUpdateEvent",
1113 | "VoiceServerUpdateEvent",
1114 | "WebhooksUpdateEvent",
1115 | "event_classes",
1116 | ]
1117 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/exception.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import Optional
3 |
4 | from nonebot.drivers import Response
5 | from nonebot.exception import (
6 | ActionFailed as BaseActionFailed,
7 | AdapterException,
8 | ApiNotAvailable as BaseApiNotAvailable,
9 | NetworkError as BaseNetworkError,
10 | NoLogException as BaseNoLogException,
11 | )
12 |
13 |
14 | class DiscordAdapterException(AdapterException):
15 | def __init__(self):
16 | super().__init__("Discord")
17 |
18 |
19 | class NoLogException(BaseNoLogException, DiscordAdapterException):
20 | pass
21 |
22 |
23 | class ActionFailed(BaseActionFailed, DiscordAdapterException):
24 | def __init__(self, response: Response):
25 | self.status_code: int = response.status_code
26 | self.code: Optional[int] = None
27 | self.message: Optional[str] = None
28 | self.errors: Optional[dict] = None
29 | self.data: Optional[dict] = None
30 | if response.content:
31 | body = json.loads(response.content)
32 | self._prepare_body(body)
33 |
34 | def __repr__(self) -> str:
35 | return (
36 | f"<{self.__class__.__name__}: {self.status_code}, code={self.code}, "
37 | f"message={self.message}, data={self.data}, errors={self.errors}>"
38 | )
39 |
40 | def __str__(self):
41 | return self.__repr__()
42 |
43 | def _prepare_body(self, body: dict):
44 | self.code = body.get("code")
45 | self.message = body.get("message")
46 | self.errors = body.get("errors")
47 | self.data = body.get("data")
48 |
49 |
50 | class UnauthorizedException(ActionFailed):
51 | pass
52 |
53 |
54 | class RateLimitException(ActionFailed):
55 | pass
56 |
57 |
58 | class NetworkError(BaseNetworkError, DiscordAdapterException):
59 | def __init__(self, msg: Optional[str] = None):
60 | super().__init__()
61 | self.msg: Optional[str] = msg
62 | """错误原因"""
63 |
64 | def __repr__(self):
65 | return f""
66 |
67 | def __str__(self):
68 | return self.__repr__()
69 |
70 |
71 | class ApiNotAvailable(BaseApiNotAvailable, DiscordAdapterException):
72 | pass
73 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/message.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Iterable
2 | from dataclasses import dataclass
3 | import datetime
4 | import re
5 | from typing import (
6 | TYPE_CHECKING,
7 | Any,
8 | Literal,
9 | Optional,
10 | TypedDict,
11 | Union,
12 | overload,
13 | )
14 | from typing_extensions import override
15 |
16 | from nonebot.adapters import (
17 | Message as BaseMessage,
18 | MessageSegment as BaseMessageSegment,
19 | )
20 |
21 | from .api import (
22 | UNSET,
23 | ActionRow,
24 | AttachmentSend,
25 | Button,
26 | Component,
27 | DirectComponent,
28 | Embed,
29 | File,
30 | MessageGet,
31 | MessageReference,
32 | SelectMenu,
33 | Snowflake,
34 | SnowflakeType,
35 | TimeStampStyle,
36 | )
37 | from .utils import unescape
38 |
39 |
40 | class MessageSegment(BaseMessageSegment["Message"]):
41 | @classmethod
42 | @override
43 | def get_message_class(cls) -> type["Message"]:
44 | return Message
45 |
46 | @staticmethod
47 | def attachment(
48 | file: Union[str, File, AttachmentSend],
49 | description: Optional[str] = None,
50 | content: Optional[bytes] = None,
51 | ) -> "AttachmentSegment":
52 | if isinstance(file, str):
53 | _filename = file
54 | _description = description
55 | _content = content
56 | elif isinstance(file, File):
57 | _filename = file.filename
58 | _description = description
59 | _content = file.content
60 | elif isinstance(file, AttachmentSend):
61 | _filename = file.filename
62 | _description = file.description
63 | _content = content
64 | else:
65 | raise TypeError("file must be str, File or AttachmentSend")
66 | if _content is None:
67 | return AttachmentSegment(
68 | "attachment",
69 | {
70 | "attachment": AttachmentSend(
71 | filename=_filename, description=_description
72 | ),
73 | "file": None,
74 | },
75 | )
76 | else:
77 | return AttachmentSegment(
78 | "attachment",
79 | {
80 | "attachment": AttachmentSend(
81 | filename=_filename, description=_description
82 | ),
83 | "file": File(filename=_filename, content=_content),
84 | },
85 | )
86 |
87 | @staticmethod
88 | def sticker(sticker_id: SnowflakeType) -> "StickerSegment":
89 | return StickerSegment("sticker", {"id": Snowflake(sticker_id)})
90 |
91 | @staticmethod
92 | def embed(embed: Embed) -> "EmbedSegment":
93 | return EmbedSegment("embed", {"embed": embed})
94 |
95 | @staticmethod
96 | def component(component: Component):
97 | if isinstance(component, (Button, SelectMenu)):
98 | component_ = ActionRow(components=[component])
99 | else:
100 | component_ = component
101 | return ComponentSegment("component", {"component": component_})
102 |
103 | @staticmethod
104 | def custom_emoji(
105 | name: str, emoji_id: str, animated: Optional[bool] = None
106 | ) -> "CustomEmojiSegment":
107 | return CustomEmojiSegment(
108 | "custom_emoji", {"name": name, "id": emoji_id, "animated": animated}
109 | )
110 |
111 | @staticmethod
112 | def mention_user(user_id: SnowflakeType) -> "MentionUserSegment":
113 | return MentionUserSegment("mention_user", {"user_id": Snowflake(user_id)})
114 |
115 | @staticmethod
116 | def mention_role(role_id: SnowflakeType) -> "MentionRoleSegment":
117 | return MentionRoleSegment("mention_role", {"role_id": Snowflake(role_id)})
118 |
119 | @staticmethod
120 | def mention_channel(channel_id: SnowflakeType) -> "MentionChannelSegment":
121 | return MentionChannelSegment(
122 | "mention_channel", {"channel_id": Snowflake(channel_id)}
123 | )
124 |
125 | @staticmethod
126 | def mention_everyone() -> "MentionEveryoneSegment":
127 | return MentionEveryoneSegment("mention_everyone")
128 |
129 | @staticmethod
130 | def text(content: str) -> "TextSegment":
131 | return TextSegment("text", {"text": content})
132 |
133 | @staticmethod
134 | def timestamp(
135 | timestamp: Union[int, datetime.datetime], style: Optional[TimeStampStyle] = None
136 | ) -> "TimestampSegment":
137 | if isinstance(timestamp, datetime.datetime):
138 | timestamp = int(timestamp.timestamp())
139 | return TimestampSegment("timestamp", {"timestamp": timestamp, "style": style})
140 |
141 | @staticmethod
142 | @overload
143 | def reference(reference: MessageReference) -> "ReferenceSegment": ...
144 |
145 | @staticmethod
146 | @overload
147 | def reference(
148 | reference: SnowflakeType,
149 | channel_id: Optional[SnowflakeType] = None,
150 | guild_id: Optional[SnowflakeType] = None,
151 | fail_if_not_exists: Optional[bool] = None,
152 | ) -> "ReferenceSegment": ...
153 |
154 | @staticmethod
155 | def reference(
156 | reference: Union[SnowflakeType, MessageReference],
157 | channel_id: Optional[SnowflakeType] = None,
158 | guild_id: Optional[SnowflakeType] = None,
159 | fail_if_not_exists: Optional[bool] = None,
160 | ):
161 | if isinstance(reference, MessageReference):
162 | _reference = reference
163 | else:
164 | _reference = MessageReference(
165 | message_id=Snowflake(reference) if reference else UNSET,
166 | channel_id=Snowflake(channel_id) if channel_id else UNSET,
167 | guild_id=Snowflake(guild_id) if guild_id else UNSET,
168 | fail_if_not_exists=fail_if_not_exists or UNSET,
169 | )
170 |
171 | return ReferenceSegment("reference", {"reference": _reference})
172 |
173 | @override
174 | def is_text(self) -> bool:
175 | return self.type == "text"
176 |
177 |
178 | class StickerData(TypedDict):
179 | id: Snowflake
180 |
181 |
182 | @dataclass
183 | class StickerSegment(MessageSegment):
184 | if TYPE_CHECKING:
185 | type: Literal["sticker"]
186 | data: StickerData
187 |
188 | @override
189 | def __str__(self) -> str:
190 | return f""
191 |
192 |
193 | class ComponentData(TypedDict):
194 | component: DirectComponent
195 |
196 |
197 | @dataclass
198 | class ComponentSegment(MessageSegment):
199 | if TYPE_CHECKING:
200 | type: Literal["component"]
201 | data: ComponentData
202 |
203 | @override
204 | def __str__(self) -> str:
205 | return f""
206 |
207 |
208 | class CustomEmojiData(TypedDict):
209 | name: str
210 | id: str
211 | animated: Optional[bool]
212 |
213 |
214 | @dataclass
215 | class CustomEmojiSegment(MessageSegment):
216 | if TYPE_CHECKING:
217 | type: Literal["custom_emoji"]
218 | data: CustomEmojiData
219 |
220 | @override
221 | def __str__(self) -> str:
222 | if self.data.get("animated"):
223 | return f""
224 | else:
225 | return f"<:{self.data['name']}:{self.data['id']}>"
226 |
227 |
228 | class MentionUserData(TypedDict):
229 | user_id: Snowflake
230 |
231 |
232 | @dataclass
233 | class MentionUserSegment(MessageSegment):
234 | if TYPE_CHECKING:
235 | type: Literal["mention_user"]
236 | data: MentionUserData
237 |
238 | @override
239 | def __str__(self) -> str:
240 | return f"<@{self.data['user_id']}>"
241 |
242 |
243 | class MentionChannelData(TypedDict):
244 | channel_id: Snowflake
245 |
246 |
247 | @dataclass
248 | class MentionChannelSegment(MessageSegment):
249 | if TYPE_CHECKING:
250 | type: Literal["mention_channel"]
251 | data: MentionChannelData
252 |
253 | @override
254 | def __str__(self) -> str:
255 | return f"<#{self.data['channel_id']}>"
256 |
257 |
258 | class MentionRoleData(TypedDict):
259 | role_id: Snowflake
260 |
261 |
262 | @dataclass
263 | class MentionRoleSegment(MessageSegment):
264 | if TYPE_CHECKING:
265 | type: Literal["mention_role"]
266 | data: MentionRoleData
267 |
268 | @override
269 | def __str__(self) -> str:
270 | return f"<@&{self.data['role_id']}>"
271 |
272 |
273 | @dataclass
274 | class MentionEveryoneSegment(MessageSegment):
275 | if TYPE_CHECKING:
276 | type: Literal["mention_everyone"]
277 |
278 | @override
279 | def __str__(self) -> str:
280 | return "@everyone"
281 |
282 |
283 | class TimestampData(TypedDict):
284 | timestamp: int
285 | style: Optional[TimeStampStyle]
286 |
287 |
288 | @dataclass
289 | class TimestampSegment(MessageSegment):
290 | if TYPE_CHECKING:
291 | type: Literal["timestamp"]
292 | data: TimestampData
293 |
294 | @override
295 | def __str__(self) -> str:
296 | style = self.data.get("style")
297 | return (
298 | f""
305 | )
306 |
307 |
308 | class TextData(TypedDict):
309 | text: str
310 |
311 |
312 | @dataclass
313 | class TextSegment(MessageSegment):
314 | if TYPE_CHECKING:
315 | type: Literal["text"]
316 | data: TextData
317 |
318 | @override
319 | def __str__(self) -> str:
320 | return self.data["text"]
321 |
322 |
323 | class EmbedData(TypedDict):
324 | embed: Embed
325 |
326 |
327 | @dataclass
328 | class EmbedSegment(MessageSegment):
329 | if TYPE_CHECKING:
330 | type: Literal["embed"]
331 | data: EmbedData
332 |
333 | @override
334 | def __str__(self) -> str:
335 | return f""
336 |
337 |
338 | class AttachmentData(TypedDict):
339 | attachment: AttachmentSend
340 | file: Optional[File]
341 |
342 |
343 | @dataclass
344 | class AttachmentSegment(MessageSegment):
345 | if TYPE_CHECKING:
346 | type: Literal["attachment"]
347 | data: AttachmentData
348 |
349 | @override
350 | def __str__(self) -> str:
351 | return f""
352 |
353 |
354 | class ReferenceData(TypedDict):
355 | reference: MessageReference
356 |
357 |
358 | @dataclass
359 | class ReferenceSegment(MessageSegment):
360 | if TYPE_CHECKING:
361 | type: Literal["reference"]
362 | data: ReferenceData
363 |
364 | @override
365 | def __str__(self):
366 | return f""
367 |
368 |
369 | class Message(BaseMessage[MessageSegment]):
370 | @classmethod
371 | @override
372 | def get_segment_class(cls) -> type[MessageSegment]:
373 | return MessageSegment
374 |
375 | @override
376 | def __add__(
377 | self, other: Union[str, MessageSegment, Iterable[MessageSegment]]
378 | ) -> "Message":
379 | return super().__add__(
380 | MessageSegment.text(other) if isinstance(other, str) else other
381 | )
382 |
383 | @override
384 | def __radd__(
385 | self, other: Union[str, MessageSegment, Iterable[MessageSegment]]
386 | ) -> "Message":
387 | return super().__radd__(
388 | MessageSegment.text(other) if isinstance(other, str) else other
389 | )
390 |
391 | @staticmethod
392 | @override
393 | def _construct(msg: str) -> Iterable[MessageSegment]:
394 | text_begin = 0
395 | for embed in re.finditer(
396 | r"<(?P(@!|@&|@|#|/|:|a:|t:))(?P[^<]+?)>",
397 | msg,
398 | ):
399 | if content := msg[text_begin : embed.pos + embed.start()]:
400 | yield MessageSegment.text(unescape(content))
401 | text_begin = embed.pos + embed.end()
402 | if embed.group("type") in ("@!", "@"):
403 | yield MessageSegment.mention_user(Snowflake(embed.group("param")))
404 | elif embed.group("type") == "@&":
405 | yield MessageSegment.mention_role(Snowflake(embed.group("param")))
406 | elif embed.group("type") == "#":
407 | yield MessageSegment.mention_channel(Snowflake(embed.group("param")))
408 | elif embed.group("type") == "/":
409 | # TODO: slash command
410 | pass
411 | elif embed.group("type") in (":", "a:"):
412 | if len(cut := embed.group("param").split(":")) == 2:
413 | yield MessageSegment.custom_emoji(
414 | cut[0], cut[1], embed.group("type") == "a:"
415 | )
416 | else:
417 | yield MessageSegment.text(unescape(embed.group()))
418 | else:
419 | if (
420 | len(cut := embed.group("param").split(":")) == 2
421 | and cut[0].isdigit()
422 | ):
423 | yield MessageSegment.timestamp(int(cut[0]), TimeStampStyle(cut[1]))
424 | elif embed.group().isdigit():
425 | yield MessageSegment.timestamp(int(embed.group()))
426 | else:
427 | yield MessageSegment.text(unescape(embed.group()))
428 | if content := msg[text_begin:]:
429 | yield MessageSegment.text(unescape(content))
430 |
431 | @classmethod
432 | def from_guild_message(cls, message: MessageGet) -> "Message":
433 | msg = Message()
434 | if message.mention_everyone:
435 | msg.append(MessageSegment.mention_everyone())
436 | if message.content:
437 | msg.extend(Message(message.content))
438 | if message.attachments:
439 | msg.extend(
440 | MessageSegment.attachment(
441 | AttachmentSend(
442 | filename=attachment.filename,
443 | description=(
444 | attachment.description
445 | if isinstance(attachment.description, str)
446 | else None
447 | ),
448 | )
449 | )
450 | for attachment in message.attachments
451 | )
452 | if message.embeds:
453 | msg.extend(MessageSegment.embed(embed) for embed in message.embeds)
454 | if message.components:
455 | msg.extend(
456 | MessageSegment.component(component) for component in message.components
457 | )
458 | return msg
459 |
460 | def extract_content(self) -> str:
461 | return "".join(
462 | str(seg)
463 | for seg in self
464 | if seg.type
465 | in (
466 | "text",
467 | "custom_emoji",
468 | "mention_user",
469 | "mention_role",
470 | "mention_everyone",
471 | "mention_channel",
472 | "timestamp",
473 | )
474 | )
475 |
476 |
477 | def parse_message(message: Union[Message, MessageSegment, str]) -> dict[str, Any]:
478 | message = MessageSegment.text(message) if isinstance(message, str) else message
479 | message = message if isinstance(message, Message) else Message(message)
480 |
481 | content = message.extract_content() or None
482 | if embeds := (message["embed"] or None):
483 | embeds = [embed.data["embed"] for embed in embeds]
484 | if reference := (message["reference"] or None):
485 | reference = reference[-1].data["reference"]
486 | if components := (message["component"] or None):
487 | components = [component.data["component"] for component in components]
488 | if sticker_ids := (message["sticker"] or None):
489 | sticker_ids = [sticker.data["id"] for sticker in sticker_ids]
490 |
491 | attachments = None
492 | files = None
493 | if attachments_segment := (message["attachment"] or None):
494 | attachments = [
495 | attachment.data["attachment"] for attachment in attachments_segment
496 | ]
497 | files = [
498 | attachment.data["file"]
499 | for attachment in attachments_segment
500 | if attachment.data["file"] is not None
501 | ]
502 | return {
503 | k: v
504 | for k, v in {
505 | "content": content,
506 | "embeds": embeds,
507 | "message_reference": reference,
508 | "components": components,
509 | "sticker_ids": sticker_ids,
510 | "files": files,
511 | "attachments": attachments,
512 | }.items()
513 | if v is not None
514 | }
515 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/payload.py:
--------------------------------------------------------------------------------
1 | from enum import IntEnum
2 | from typing import Annotated, Optional, Union
3 | from typing_extensions import Literal
4 |
5 | from nonebot.compat import PYDANTIC_V2, ConfigDict
6 |
7 | from pydantic import BaseModel, Field
8 |
9 | from .api.model import (
10 | Hello as HelloData,
11 | Identify as IdentifyData,
12 | Resume as ResumeData,
13 | )
14 |
15 |
16 | class Opcode(IntEnum):
17 | DISPATCH = 0
18 | HEARTBEAT = 1
19 | IDENTIFY = 2
20 | RESUME = 6
21 | RECONNECT = 7
22 | INVALID_SESSION = 9
23 | HELLO = 10
24 | HEARTBEAT_ACK = 11
25 |
26 |
27 | class Payload(BaseModel):
28 | if PYDANTIC_V2:
29 | model_config = ConfigDict(extra="allow", populate_by_name=True) # type: ignore
30 |
31 | else:
32 |
33 | class Config(ConfigDict):
34 | extra = "allow"
35 | allow_population_by_field_name = True
36 |
37 |
38 | class Dispatch(Payload):
39 | opcode: Literal[Opcode.DISPATCH] = Field(Opcode.DISPATCH, alias="op")
40 | data: dict = Field(alias="d")
41 | sequence: int = Field(alias="s")
42 | type: str = Field(alias="t")
43 |
44 |
45 | class Heartbeat(Payload):
46 | opcode: Literal[Opcode.HEARTBEAT] = Field(Opcode.HEARTBEAT, alias="op")
47 | data: Optional[int] = Field(None, alias="d")
48 |
49 |
50 | class Identify(Payload):
51 | opcode: Literal[Opcode.IDENTIFY] = Field(Opcode.IDENTIFY, alias="op")
52 | data: IdentifyData = Field(alias="d")
53 |
54 |
55 | class Resume(Payload):
56 | opcode: Literal[Opcode.RESUME] = Field(Opcode.RESUME, alias="op")
57 | data: ResumeData = Field(alias="d")
58 |
59 |
60 | class Reconnect(Payload):
61 | opcode: Literal[Opcode.RECONNECT] = Field(Opcode.RECONNECT, alias="op")
62 |
63 |
64 | class InvalidSession(Payload):
65 | opcode: Literal[Opcode.INVALID_SESSION] = Field(Opcode.INVALID_SESSION, alias="op")
66 |
67 |
68 | class Hello(Payload):
69 | opcode: Literal[Opcode.HELLO] = Field(Opcode.HELLO, alias="op")
70 | data: HelloData = Field(alias="d")
71 |
72 |
73 | class HeartbeatAck(Payload):
74 | opcode: Literal[Opcode.HEARTBEAT_ACK] = Field(Opcode.HEARTBEAT_ACK, alias="op")
75 |
76 |
77 | PayloadType = Union[
78 | Annotated[
79 | Union[Dispatch, Reconnect, InvalidSession, Hello, HeartbeatAck],
80 | Field(discriminator="opcode"),
81 | ],
82 | Payload,
83 | ]
84 |
--------------------------------------------------------------------------------
/nonebot/adapters/discord/utils.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Optional, Union
2 | import zlib
3 |
4 | from nonebot.compat import model_dump as model_dump_
5 | from nonebot.utils import logger_wrapper
6 |
7 | from pydantic import BaseModel
8 |
9 | from .api.types import UNSET
10 |
11 | log = logger_wrapper("Discord")
12 |
13 |
14 | def exclude_unset_data(data: Any) -> Any:
15 | if isinstance(data, dict):
16 | return data.__class__(
17 | (k, exclude_unset_data(v)) for k, v in data.items() if v is not UNSET
18 | )
19 | elif isinstance(data, list):
20 | return data.__class__(exclude_unset_data(i) for i in data)
21 | elif data is UNSET:
22 | return None
23 | return data
24 |
25 |
26 | def model_dump(
27 | model: BaseModel,
28 | include: Optional[set[str]] = None,
29 | exclude: Optional[set[str]] = None,
30 | by_alias: bool = False,
31 | exclude_unset: bool = False,
32 | exclude_defaults: bool = False,
33 | exclude_none: bool = False,
34 | ) -> dict[str, Any]:
35 | data = model_dump_(
36 | model,
37 | include=include,
38 | exclude=exclude,
39 | by_alias=by_alias,
40 | exclude_unset=exclude_unset,
41 | exclude_defaults=exclude_defaults,
42 | exclude_none=exclude_none,
43 | )
44 | if exclude_none or exclude_unset:
45 | return exclude_unset_data(data)
46 | else:
47 | return data
48 |
49 |
50 | def escape(s: str) -> str:
51 | return s.replace("&", "&").replace("<", "<").replace(">", ">")
52 |
53 |
54 | def unescape(s: str) -> str:
55 | return s.replace("<", "<").replace(">", ">").replace("&", "&")
56 |
57 |
58 | def decompress_data(data: Union[str, bytes], compress: bool) -> Union[str, bytes]:
59 | return zlib.decompress(data) if compress else data # type: ignore
60 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "nonebot-adapter-discord"
3 | version = "0.1.8"
4 | description = "Discord adapter for nonebot2"
5 | authors = ["CMHopeSunshine <277073121@qq.com>", "yanyongyu "]
6 | license = "MIT"
7 | readme = "README.md"
8 | homepage = "https://github.com/nonebot/adapter-discord"
9 | repository = "https://github.com/nonebot/adapter-discord"
10 | documentation = "https://github.com/nonebot/adapter-discord"
11 | keywords = ["nonebot", "discord", "bot"]
12 |
13 | packages = [{ include = "nonebot" }]
14 |
15 | [tool.poetry.dependencies]
16 | python = "^3.9"
17 | nonebot2 = "^2.2.1"
18 |
19 | [tool.poetry.group.dev.dependencies]
20 | ruff = "^0.1.4"
21 | nonemoji = "^0.1.2"
22 | pre-commit = "^3.1.0"
23 |
24 | [tool.ruff]
25 | select = ["E", "W", "F", "I", "UP", "C", "T", "PYI", "PT"]
26 | ignore = ["E402", "F403", "F405", "C901", "PYI021", "PYI048", "W191", "E501"]
27 | line-length = 88
28 | target-version = "py39"
29 | ignore-init-module-imports = true
30 |
31 |
32 | [tool.ruff.isort]
33 | force-sort-within-sections = true
34 | extra-standard-library = ["typing_extensions"]
35 | combine-as-imports = true
36 | order-by-type = true
37 | relative-imports-order = "closest-to-furthest"
38 | section-order = [
39 | "future",
40 | "standard-library",
41 | "first-party",
42 | "third-party",
43 | "local-folder",
44 | ]
45 |
46 | [build-system]
47 | requires = ["poetry_core>=1.0.0"]
48 | build-backend = "poetry.core.masonry.api"
49 |
--------------------------------------------------------------------------------