simcord 2.0.1__tar.gz → 2.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simcord-2.0.1 → simcord-2.0.2}/CHANGELOG.md +9 -0
- {simcord-2.0.1 → simcord-2.0.2}/PKG-INFO +1 -1
- {simcord-2.0.1 → simcord-2.0.2}/pyproject.toml +1 -1
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/guilds.py +17 -4
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/serializers.py +12 -1
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/guilds.py +2 -2
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_edge_cases.py +6 -1
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_roles_and_ordering.py +75 -0
- {simcord-2.0.1 → simcord-2.0.2}/uv.lock +1 -1
- {simcord-2.0.1 → simcord-2.0.2}/.github/ISSUE_TEMPLATE/bug.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/.github/ISSUE_TEMPLATE/feature.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/.github/ISSUE_TEMPLATE/parity-gap.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/.github/dependabot.yml +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/.github/workflows/ci.yml +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/.github/workflows/dpy-master.yml +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/.github/workflows/release.yml +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/.gitignore +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/.pre-commit-config.yaml +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/.python-version +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/.readthedocs.yaml +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/CODE_OF_CONDUCT.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/CONTRIBUTING.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/LICENSE +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/README.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/SECURITY.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/benchmarks/_helpers.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/benchmarks/test_dispatch_benchmarks.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/benchmarks/test_perf_guards.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/changes/README.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/api.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/architecture.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/assets/simcord-terminal-demo.gif +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/assets/simcord-transparent.webp +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/assets/social-card.png +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/changelog.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/concepts.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/cookbook.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/faq.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/ai-coding-agents.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/components.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/diagnostics.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/fixtures.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/intents.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/interactions.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/messages.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/mocks-vs-simulation.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/moderation-and-guild.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/permissions.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/test-without-token.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/testing-discord-py-bots.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/testing-slash-commands.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/threads-reactions-dms.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/guides/time-control.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/index.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/installation.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/llms.txt +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/parity-matrix.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/performance.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/quickstart.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/robots.txt +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/docs/stability.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/examples/README.md +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/examples/__init__.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/examples/bot.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/examples/conftest.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/examples/test_bot.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/mkdocs.yml +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/overrides/main.html +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/scripts/check_discoverability.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/__init__.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/_dpy_internals.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/actors.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/asserts.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/__init__.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/cdn.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/errors.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/__init__.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/auditlog.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/automod.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/channel.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/expression.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/guild.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/interaction.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/invite.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/member.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/message.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/role.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/scheduled_event.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/stage_instance.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/user.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/voice.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/webhook.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/__init__.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/audit.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/base.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/channels.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/commands.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/expressions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/invites.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/messages.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/permissions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/polls.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/reactions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/scheduled_events.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/voice.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/webhooks.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/permissions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/state.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/builders.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/components.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/enums.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/env.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/gateway.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/__init__.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/_helpers.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/client.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/router.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/__init__.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/application.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/auditlogs.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/automod.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/channels.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/commands.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/expressions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/interactions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/invites.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/messages.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/reactions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/scheduled_events.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/stage_instances.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/intents.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/interactions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/parity.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/py.typed +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/pytest_plugin.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/src/simcord/results.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/conformance/_world.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/conformance/test_field_honesty.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/conformance/test_field_honesty_fuzz.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/conformance/test_route_coverage.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/conformance/test_serializers.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/conftest.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/fixtures/__init__.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/fixtures/sample_bot/__init__.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/fixtures/sample_bot/events.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/fixtures/sample_bot/general.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/fixtures/sample_bot/interactions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/fixtures/sample_bot/moderation.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_app_commands.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_application_emojis.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_asserts.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_audit_logs.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_automod.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_bot_users_and_webhooks.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_channels.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_command_permissions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_common_bot_sweep.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_components_v2_interactions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_components_v2_messages.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_coverage_sweep.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_diagnostics.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_error_capture_and_edits.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_expressions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_forum.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_gateway_and_automod_exempt.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_guilds.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_intents.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_interaction_followups.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_interactions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_invites.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_lifecycle.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_members.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_message_routes.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_messages.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_misc_branches.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_moderation.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_polls.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_reactions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_scheduled_events.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_settlement_contract.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_setup_hook_suspension.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_sharding.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_stage_instances.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_threads.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_time.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_transport_edges.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_voice_state.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_world_helpers.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_assert_helpers.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_backend_lookup_errors.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_components_v2_validation.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_errors.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_intent_rules.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_parity.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_parity_tooling.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_permission_validators.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_permissions.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_pytest_plugin.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_router.py +0 -0
- {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_version.py +0 -0
|
@@ -4,6 +4,15 @@ This changelog is generated with [towncrier](https://towncrier.readthedocs.io/).
|
|
|
4
4
|
|
|
5
5
|
<!-- towncrier release notes start -->
|
|
6
6
|
|
|
7
|
+
## 2.0.2 (2026-09-18)
|
|
8
|
+
|
|
9
|
+
### Bug fixes
|
|
10
|
+
|
|
11
|
+
- Fixed ``create_role`` leaving stale ``Role.position`` values in the bot's cache: inserting a role bumps every existing role's position, and the backend now announces each shift with ``GUILD_ROLE_UPDATE`` after ``GUILD_ROLE_CREATE``, as real Discord does. ``Member.top_role``, ``Role`` comparisons, and hierarchy checks now see the true ordering after role creation.
|
|
12
|
+
- Fixed ``role.colour`` always reading ``0`` in the bot's cache: role payloads now carry the ``colors`` object (``primary_color``/``secondary_color``/``tertiary_color``) that discord.py reads, alongside the deprecated flat ``color`` field.
|
|
13
|
+
- Fixed guild role listings to match real Discord's ordering: ``GET``/``PATCH /guilds/{id}/roles`` responses and the ``roles`` array inside guild payloads (``GUILD_CREATE``/``GUILD_UPDATE`` events and guild REST responses) are now sorted by role id ascending instead of insertion order (observable when a guild is created with an explicit id, which can make ``@everyone``'s snowflake the largest).
|
|
14
|
+
|
|
15
|
+
|
|
7
16
|
## 2.0.1 (2026-09-10)
|
|
8
17
|
|
|
9
18
|
### Bug fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simcord
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Offline discord.py bot testing with pytest. No network, token, or test server.
|
|
5
5
|
Project-URL: Homepage, https://github.com/SilentHacks/simcord
|
|
6
6
|
Project-URL: Documentation, https://simcord.readthedocs.io/
|
|
@@ -180,9 +180,23 @@ class GuildMixin(BackendBase):
|
|
|
180
180
|
|
|
181
181
|
# ----------------------------------------------------------------- roles
|
|
182
182
|
|
|
183
|
+
def announce_role_updates(self, guild_id: int, roles: Iterable[Role]) -> None:
|
|
184
|
+
"""Announce each role's current state with GUILD_ROLE_UPDATE.
|
|
185
|
+
|
|
186
|
+
Real Discord emits an update for every role whose position changed as a
|
|
187
|
+
side effect of another operation; without them a client's cache keeps
|
|
188
|
+
stale positions, so position-driven mutations must announce their shifts.
|
|
189
|
+
"""
|
|
190
|
+
for role in list(roles):
|
|
191
|
+
self.emit(
|
|
192
|
+
"GUILD_ROLE_UPDATE",
|
|
193
|
+
{"guild_id": str(guild_id), "role": serializers.role_payload(role)},
|
|
194
|
+
)
|
|
195
|
+
|
|
183
196
|
def create_role(self, guild_id: int, name: str, *, permissions: int = 0, **fields: Any) -> Role:
|
|
184
197
|
guild = self.get_guild(guild_id)
|
|
185
198
|
position = fields.pop("position", None)
|
|
199
|
+
shifted: list[Role] = []
|
|
186
200
|
if position is None:
|
|
187
201
|
# New roles insert just above @everyone, pushing existing roles up
|
|
188
202
|
# (so the bot's integration role stays on top), as on real Discord.
|
|
@@ -190,6 +204,7 @@ class GuildMixin(BackendBase):
|
|
|
190
204
|
for existing in guild.roles.values():
|
|
191
205
|
if existing.position >= 1:
|
|
192
206
|
existing.position += 1
|
|
207
|
+
shifted.append(existing)
|
|
193
208
|
role = Role(
|
|
194
209
|
id=self.snowflake(),
|
|
195
210
|
name=name,
|
|
@@ -202,6 +217,7 @@ class GuildMixin(BackendBase):
|
|
|
202
217
|
"GUILD_ROLE_CREATE",
|
|
203
218
|
{"guild_id": str(guild_id), "role": serializers.role_payload(role)},
|
|
204
219
|
)
|
|
220
|
+
self.announce_role_updates(guild_id, shifted)
|
|
205
221
|
return role
|
|
206
222
|
|
|
207
223
|
def edit_role(self, guild_id: int, role_id: int, changes: Mapping[str, Any]) -> Role:
|
|
@@ -229,10 +245,7 @@ class GuildMixin(BackendBase):
|
|
|
229
245
|
if role.position != new_position:
|
|
230
246
|
role.position = new_position
|
|
231
247
|
changed.append(role)
|
|
232
|
-
|
|
233
|
-
self.emit(
|
|
234
|
-
"GUILD_ROLE_UPDATE", {"guild_id": str(guild_id), "role": serializers.role_payload(role)}
|
|
235
|
-
)
|
|
248
|
+
self.announce_role_updates(guild_id, changed)
|
|
236
249
|
return guild
|
|
237
250
|
|
|
238
251
|
def delete_role(self, guild_id: int, role_id: int) -> None:
|
|
@@ -88,6 +88,11 @@ def role_payload(role: Role) -> role_types.Role:
|
|
|
88
88
|
"permissions": str(role.permissions),
|
|
89
89
|
"position": role.position,
|
|
90
90
|
"color": role.color,
|
|
91
|
+
"colors": {
|
|
92
|
+
"primary_color": role.color,
|
|
93
|
+
"secondary_color": None,
|
|
94
|
+
"tertiary_color": None,
|
|
95
|
+
},
|
|
91
96
|
"hoist": role.hoist,
|
|
92
97
|
"managed": role.managed,
|
|
93
98
|
"mentionable": role.mentionable,
|
|
@@ -98,6 +103,12 @@ def role_payload(role: Role) -> role_types.Role:
|
|
|
98
103
|
)
|
|
99
104
|
|
|
100
105
|
|
|
106
|
+
def roles_payload(guild: Guild) -> list[role_types.Role]:
|
|
107
|
+
# Discord serves a guild's role list sorted by role id ascending (the
|
|
108
|
+
# @everyone role shares the guild id, so it can sort anywhere).
|
|
109
|
+
return [role_payload(r) for r in sorted(guild.roles.values(), key=lambda r: r.id)]
|
|
110
|
+
|
|
111
|
+
|
|
101
112
|
def member_payload(
|
|
102
113
|
backend: BackendBase, guild: Guild, member: Member, *, with_user: bool = True
|
|
103
114
|
) -> member_types.MemberWithUser:
|
|
@@ -355,7 +366,7 @@ def guild_create_payload(backend: BackendBase, guild: Guild) -> guild_types.Guil
|
|
|
355
366
|
"features": [],
|
|
356
367
|
"emojis": [guild_emoji_payload(backend, e) for e in guild.emojis.values()],
|
|
357
368
|
"stickers": [sticker_payload(backend, s) for s in guild.stickers.values()],
|
|
358
|
-
"roles":
|
|
369
|
+
"roles": roles_payload(guild),
|
|
359
370
|
"member_count": len(guild.members),
|
|
360
371
|
"large": False,
|
|
361
372
|
"unavailable": False,
|
|
@@ -640,7 +640,7 @@ def delete_role(ctx: RequestContext) -> Any:
|
|
|
640
640
|
@route("GET", "/guilds/{guild_id}/roles")
|
|
641
641
|
def get_roles(ctx: RequestContext) -> Any:
|
|
642
642
|
guild = ctx.backend.get_guild(ctx.int_arg("guild_id"))
|
|
643
|
-
return [dict(
|
|
643
|
+
return [dict(r) for r in serializers.roles_payload(guild)]
|
|
644
644
|
|
|
645
645
|
|
|
646
646
|
@route("GET", "/guilds/{guild_id}/roles/{role_id}")
|
|
@@ -668,4 +668,4 @@ def move_role_positions(ctx: RequestContext) -> Any:
|
|
|
668
668
|
backend.require_role_assignable(guild_id, bot_id, int(item["id"]))
|
|
669
669
|
backend.require_position_assignable(guild_id, bot_id, int(item["position"]))
|
|
670
670
|
guild = backend.reorder_roles(guild_id, positions)
|
|
671
|
-
return [dict(
|
|
671
|
+
return [dict(r) for r in serializers.roles_payload(guild)]
|
|
@@ -69,7 +69,12 @@ async def test_role_reorder_changes_positions(env):
|
|
|
69
69
|
await env.settle()
|
|
70
70
|
|
|
71
71
|
assert env.backend.get_role(env.guild.id, low.id).position == high_pos
|
|
72
|
-
|
|
72
|
+
# The reorder itself announced the moved role (create_role updates alone
|
|
73
|
+
# would leave this assertion passing otherwise).
|
|
74
|
+
assert any(
|
|
75
|
+
name == "GUILD_ROLE_UPDATE" and payload["role"]["id"] == str(low.id)
|
|
76
|
+
for _, name, payload in env.backend.transcript
|
|
77
|
+
)
|
|
73
78
|
|
|
74
79
|
|
|
75
80
|
async def test_second_stage_instance_rejected(env):
|
|
@@ -54,6 +54,81 @@ async def test_edit_role_positions(env):
|
|
|
54
54
|
assert guild.get_role(low.id).position == high_pos
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
async def test_create_role_announces_position_shifts(env):
|
|
58
|
+
guild = env.bot.get_guild(env.guild.id)
|
|
59
|
+
await guild.create_role(name="admin")
|
|
60
|
+
await guild.create_role(name="mod")
|
|
61
|
+
await env.settle()
|
|
62
|
+
|
|
63
|
+
# Each create inserts at position 1 and bumps every existing role up; the
|
|
64
|
+
# backend announces each shift with GUILD_ROLE_UPDATE (after the create, as
|
|
65
|
+
# real Discord does), so every cached position matches the backend's.
|
|
66
|
+
backend_guild = env.backend.get_guild(env.guild.id)
|
|
67
|
+
for backend_role in backend_guild.roles.values():
|
|
68
|
+
cached = guild.get_role(backend_role.id)
|
|
69
|
+
assert cached is not None
|
|
70
|
+
assert cached.position == backend_role.position
|
|
71
|
+
assert backend_guild.roles[guild.id].position == 0
|
|
72
|
+
|
|
73
|
+
# Each create lands first, then the shift updates it caused.
|
|
74
|
+
role_events = [
|
|
75
|
+
(name, payload["role"]["name"])
|
|
76
|
+
for _, name, payload in env.backend.transcript
|
|
77
|
+
if name in ("GUILD_ROLE_CREATE", "GUILD_ROLE_UPDATE")
|
|
78
|
+
]
|
|
79
|
+
assert role_events[:2] == [("GUILD_ROLE_CREATE", "admin"), ("GUILD_ROLE_UPDATE", "TestBot")]
|
|
80
|
+
assert role_events[2] == ("GUILD_ROLE_CREATE", "mod")
|
|
81
|
+
assert all(name == "GUILD_ROLE_UPDATE" for name, _ in role_events[3:])
|
|
82
|
+
|
|
83
|
+
# The bot's managed role rides on top of the hierarchy, so hierarchy checks
|
|
84
|
+
# like "bot must outrank target" see a true ordering.
|
|
85
|
+
assert guild.me.top_role.position == max(r.position for r in backend_guild.roles.values())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def test_delete_role_leaves_position_gap(env):
|
|
89
|
+
guild = env.bot.get_guild(env.guild.id)
|
|
90
|
+
low = await guild.create_role(name="low")
|
|
91
|
+
mid = await guild.create_role(name="mid")
|
|
92
|
+
await env.settle()
|
|
93
|
+
|
|
94
|
+
await mid.delete()
|
|
95
|
+
await env.settle()
|
|
96
|
+
|
|
97
|
+
# Discord does not renumber positions on delete: the hole at 1 stays a hole.
|
|
98
|
+
assert env.backend.get_role(guild.id, low.id).position == 2
|
|
99
|
+
assert guild.get_role(low.id).position == 2
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def test_role_colour_reaches_the_cache(env):
|
|
103
|
+
guild = env.bot.get_guild(env.guild.id)
|
|
104
|
+
role = await guild.create_role(name="Tinted", colour=discord.Colour(0x112233))
|
|
105
|
+
await env.settle()
|
|
106
|
+
|
|
107
|
+
# discord.py reads the role "colors" object (primary/secondary/tertiary);
|
|
108
|
+
# the deprecated flat "color" field alone leaves every cached colour at 0.
|
|
109
|
+
assert role.colour == discord.Colour(0x112233)
|
|
110
|
+
cached = guild.get_role(role.id)
|
|
111
|
+
assert cached.colour == discord.Colour(0x112233)
|
|
112
|
+
assert cached.secondary_colour is None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def test_guild_roles_list_sorted_by_id(env):
|
|
116
|
+
odd = env.create_guild("Odd", id=8_000_000_000_000_000_000)
|
|
117
|
+
await env.settle()
|
|
118
|
+
guild = env.bot.get_guild(odd.id)
|
|
119
|
+
await guild.create_role(name="one")
|
|
120
|
+
await guild.create_role(name="two")
|
|
121
|
+
await env.settle()
|
|
122
|
+
|
|
123
|
+
# Real Discord returns a guild's roles sorted by id ascending — with an
|
|
124
|
+
# explicit guild id the @everyone role's snowflake is the largest, so
|
|
125
|
+
# insertion order alone would wrongly put it first.
|
|
126
|
+
fetched = await guild.fetch_roles()
|
|
127
|
+
ids = [r.id for r in fetched]
|
|
128
|
+
assert ids == sorted(ids)
|
|
129
|
+
assert ids[-1] == odd.id
|
|
130
|
+
|
|
131
|
+
|
|
57
132
|
async def test_reorder_cannot_lift_role_above_bot(env):
|
|
58
133
|
guild = env.bot.get_guild(env.guild.id)
|
|
59
134
|
role = await guild.create_role(name="Climber")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|