├── .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 | nonebot-adapter-discord 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 | --------------------------------------------------------------------------------