Compare commits
3 Commits
15d95bc786
...
a702c66603
Author | SHA1 | Date | |
---|---|---|---|
a702c66603 | |||
aec0de3e58 | |||
66288ba920 |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
|||||||
|
|
@ -11,12 +11,7 @@ import json
|
|||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
from typing import Optional, Dict, Any, Union, TYPE_CHECKING, List
|
from typing import Optional, Dict, Any, Union, TYPE_CHECKING, List
|
||||||
|
|
||||||
from .errors import (
|
from .errors import * # Import all custom exceptions
|
||||||
HTTPException,
|
|
||||||
RateLimitError,
|
|
||||||
AuthenticationError,
|
|
||||||
DisagreementException,
|
|
||||||
)
|
|
||||||
from . import __version__ # For User-Agent
|
from . import __version__ # For User-Agent
|
||||||
from .rate_limiter import RateLimiter
|
from .rate_limiter import RateLimiter
|
||||||
from .interactions import InteractionResponsePayload
|
from .interactions import InteractionResponsePayload
|
||||||
@ -31,6 +26,232 @@ API_BASE_URL = "https://discord.com/api/v10" # Using API v10
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DISCORD_ERROR_CODE_TO_EXCEPTION = {
|
||||||
|
0: GeneralError,
|
||||||
|
10001: UnknownAccount,
|
||||||
|
10002: UnknownApplication,
|
||||||
|
10003: UnknownChannel,
|
||||||
|
10004: UnknownGuild,
|
||||||
|
10005: UnknownIntegration,
|
||||||
|
10006: UnknownInvite,
|
||||||
|
10007: UnknownMember,
|
||||||
|
10008: UnknownMessage,
|
||||||
|
10009: UnknownPermissionOverwrite,
|
||||||
|
10010: UnknownProvider,
|
||||||
|
10011: UnknownRole,
|
||||||
|
10012: UnknownToken,
|
||||||
|
10013: UnknownUser,
|
||||||
|
10014: UnknownEmoji,
|
||||||
|
10015: UnknownWebhook,
|
||||||
|
10016: UnknownWebhookService,
|
||||||
|
10020: UnknownSession,
|
||||||
|
10021: UnknownAsset,
|
||||||
|
10026: UnknownBan,
|
||||||
|
10027: UnknownSKU,
|
||||||
|
10028: UnknownStoreListing,
|
||||||
|
10029: UnknownEntitlement,
|
||||||
|
10030: UnknownBuild,
|
||||||
|
10031: UnknownLobby,
|
||||||
|
10032: UnknownBranch,
|
||||||
|
10033: UnknownStoreDirectoryLayout,
|
||||||
|
10036: UnknownRedistributable,
|
||||||
|
10038: UnknownGiftCode,
|
||||||
|
10049: UnknownStream,
|
||||||
|
10050: UnknownPremiumServerSubscribeCooldown,
|
||||||
|
10057: UnknownGuildTemplate,
|
||||||
|
10059: UnknownDiscoverableServerCategory,
|
||||||
|
10060: UnknownSticker,
|
||||||
|
10061: UnknownStickerPack,
|
||||||
|
10062: UnknownInteraction,
|
||||||
|
10063: UnknownApplicationCommand,
|
||||||
|
10065: UnknownVoiceState,
|
||||||
|
10066: UnknownApplicationCommandPermissions,
|
||||||
|
10067: UnknownStageInstance,
|
||||||
|
10068: UnknownGuildMemberVerificationForm,
|
||||||
|
10069: UnknownGuildWelcomeScreen,
|
||||||
|
10070: UnknownGuildScheduledEvent,
|
||||||
|
10071: UnknownGuildScheduledEventUser,
|
||||||
|
10087: UnknownTag,
|
||||||
|
10097: UnknownSound,
|
||||||
|
20001: BotsCannotUseThisEndpoint,
|
||||||
|
20002: OnlyBotsCanUseThisEndpoint,
|
||||||
|
20009: ExplicitContentCannotBeSentToTheDesiredRecipients,
|
||||||
|
20012: NotAuthorizedToPerformThisActionOnThisApplication,
|
||||||
|
20016: ActionCannotBePerformedDueToSlowmodeRateLimit,
|
||||||
|
20018: OnlyTheOwnerOfThisAccountCanPerformThisAction,
|
||||||
|
20022: MessageCannotBeEditedDueToAnnouncementRateLimits,
|
||||||
|
20024: UnderMinimumAge,
|
||||||
|
20028: ChannelHitWriteRateLimit,
|
||||||
|
20029: ServerHitWriteRateLimit,
|
||||||
|
20031: DisallowedWordsInStageTopicOrNames,
|
||||||
|
20035: GuildPremiumSubscriptionLevelTooLow,
|
||||||
|
30001: MaximumNumberOfGuildsReached,
|
||||||
|
30002: MaximumNumberOfFriendsReached,
|
||||||
|
30003: MaximumNumberOfPinsReached,
|
||||||
|
30004: MaximumNumberOfRecipientsReached,
|
||||||
|
30005: MaximumNumberOfGuildRolesReached,
|
||||||
|
30007: MaximumNumberOfWebhooksReached,
|
||||||
|
30008: MaximumNumberOfEmojisReached,
|
||||||
|
30010: MaximumNumberOfReactionsReached,
|
||||||
|
30011: MaximumNumberOfGroupDMsReached,
|
||||||
|
30013: MaximumNumberOfGuildChannelsReached,
|
||||||
|
30015: MaximumNumberOfAttachmentsInAMessageReached,
|
||||||
|
30016: MaximumNumberOfInvitesReached,
|
||||||
|
30018: MaximumNumberOfAnimatedEmojisReached,
|
||||||
|
30019: MaximumNumberOfServerMembersReached,
|
||||||
|
30030: MaximumNumberOfServerCategoriesReached,
|
||||||
|
30031: GuildAlreadyHasATemplate,
|
||||||
|
30032: MaximumNumberOfApplicationCommandsReached,
|
||||||
|
30033: MaximumNumberOfThreadParticipantsReached,
|
||||||
|
30034: MaximumNumberOfDailyApplicationCommandCreatesReached,
|
||||||
|
30035: MaximumNumberOfBansForNonGuildMembersExceeded,
|
||||||
|
30037: MaximumNumberOfBansFetchesReached,
|
||||||
|
30038: MaximumNumberOfUncompletedGuildScheduledEventsReached,
|
||||||
|
30039: MaximumNumberOfStickersReached,
|
||||||
|
30040: MaximumNumberOfPruneRequestsReached,
|
||||||
|
30042: MaximumNumberOfGuildWidgetSettingsUpdatesReached,
|
||||||
|
30045: MaximumNumberOfSoundboardSoundsReached,
|
||||||
|
30046: MaximumNumberOfEditsToMessagesOlderThan1HourReached,
|
||||||
|
30047: MaximumNumberOfPinnedThreadsInAForumChannelReached,
|
||||||
|
30048: MaximumNumberOfTagsInAForumChannelReached,
|
||||||
|
30052: BitrateIsTooHighForChannelOfThisType,
|
||||||
|
30056: MaximumNumberOfPremiumEmojisReached,
|
||||||
|
30058: MaximumNumberOfWebhooksPerGuildReached,
|
||||||
|
30061: MaximumNumberOfChannelPermissionOverwritesReached,
|
||||||
|
30062: TheChannelsForThisGuildAreTooLarge,
|
||||||
|
40001: Unauthorized,
|
||||||
|
40002: YouNeedToVerifyYourAccount,
|
||||||
|
40003: YouAreOpeningDirectMessagesTooFast,
|
||||||
|
40004: SendMessagesHasBeenTemporarilyDisabled,
|
||||||
|
40005: RequestEntityTooLarge,
|
||||||
|
40006: ThisFeatureHasBeenTemporarilyDisabledServerSide,
|
||||||
|
40007: TheUserIsBannedFromThisGuild,
|
||||||
|
40012: ConnectionHasBeenRevoked,
|
||||||
|
40018: OnlyConsumableSKUsCanBeConsumed,
|
||||||
|
40019: YouCanOnlyDeleteSandboxEntitlements,
|
||||||
|
40032: TargetUserIsNotConnectedToVoice,
|
||||||
|
40033: ThisMessageHasAlreadyBeenCrossposted,
|
||||||
|
40041: AnApplicationCommandWithThatNameAlreadyExists,
|
||||||
|
40043: ApplicationInteractionFailedToSend,
|
||||||
|
40058: CannotSendAMessageInAForumChannel,
|
||||||
|
40060: InteractionHasAlreadyBeenAcknowledged,
|
||||||
|
40061: TagNamesMustBeUnique,
|
||||||
|
40062: ServiceResourceIsBeingRateLimited,
|
||||||
|
40066: ThereAreNoTagsAvailableThatCanBeSetByNonModerators,
|
||||||
|
40067: ATagIsRequiredToCreateAForumPostInThisChannel,
|
||||||
|
40074: AnEntitlementHasAlreadyBeenGrantedForThisResource,
|
||||||
|
40094: ThisInteractionHasHitTheMaximumNumberOfFollowUpMessages,
|
||||||
|
40333: CloudflareIsBlockingYourRequest,
|
||||||
|
50001: MissingAccess,
|
||||||
|
50002: InvalidAccountType,
|
||||||
|
50003: CannotExecuteActionOnADMChannel,
|
||||||
|
50004: GuildWidgetDisabled,
|
||||||
|
50005: CannotEditAMessageAuthoredByAnotherUser,
|
||||||
|
50006: CannotSendAnEmptyMessage,
|
||||||
|
50007: CannotSendMessagesToThisUser,
|
||||||
|
50008: CannotSendMessagesInANonTextChannel,
|
||||||
|
50009: ChannelVerificationLevelIsTooHighForYouToGainAccess,
|
||||||
|
50010: OAuth2ApplicationDoesNotHaveABot,
|
||||||
|
50011: OAuth2ApplicationLimitReached,
|
||||||
|
50012: InvalidOAuth2State,
|
||||||
|
50013: YouLackPermissionsToPerformThatAction,
|
||||||
|
50014: InvalidAuthenticationTokenProvided,
|
||||||
|
50015: NoteWasTooLong,
|
||||||
|
50016: ProvidedTooFewOrTooManyMessagesToDelete,
|
||||||
|
50017: InvalidMFALevel,
|
||||||
|
50019: AMessageCanOnlyBePinnedToTheChannelItWasSentIn,
|
||||||
|
50020: InviteCodeWasEitherInvalidOrTaken,
|
||||||
|
50021: CannotExecuteActionOnASystemMessage,
|
||||||
|
50024: CannotExecuteActionOnThisChannelType,
|
||||||
|
50025: InvalidOAuth2AccessTokenProvided,
|
||||||
|
50026: MissingRequiredOAuth2Scope,
|
||||||
|
50027: InvalidWebhookTokenProvided,
|
||||||
|
50028: InvalidRole,
|
||||||
|
50033: InvalidRecipients,
|
||||||
|
50034: AMessageProvidedWasTooOldToBulkDelete,
|
||||||
|
50035: InvalidFormBody,
|
||||||
|
50036: AnInviteWasAcceptedToAGuildTheApplicationBotIsNotIn,
|
||||||
|
50039: InvalidActivityAction,
|
||||||
|
50041: InvalidAPIVersionProvided,
|
||||||
|
50045: FileUploadedExceedsTheMaximumSize,
|
||||||
|
50046: InvalidFileUploaded,
|
||||||
|
50054: CannotSelfRedeemThisGift,
|
||||||
|
50055: InvalidGuild,
|
||||||
|
50057: InvalidSKU,
|
||||||
|
50067: InvalidRequestOrigin,
|
||||||
|
50068: InvalidMessageType,
|
||||||
|
50070: PaymentSourceRequiredToRedeemGift,
|
||||||
|
50073: CannotModifyASystemWebhook,
|
||||||
|
50074: CannotDeleteAChannelRequiredForCommunityGuilds,
|
||||||
|
50080: CannotEditStickersWithinAMessage,
|
||||||
|
50081: InvalidStickerSent,
|
||||||
|
50083: TriedToPerformAnOperationOnAnArchivedThread,
|
||||||
|
50085: InvalidThreadNotificationSettings,
|
||||||
|
50086: BeforeValueIsEarlierThanTheThreadCreationDate,
|
||||||
|
50087: CommunityServerChannelsMustBeTextChannels,
|
||||||
|
50091: TheEntityTypeOfTheEventIsDifferentFromTheEntityYouAreTryingToStartTheEventFor,
|
||||||
|
50095: ThisServerIsNotAvailableInYourLocation,
|
||||||
|
50097: ThisServerNeedsMonetizationEnabledInOrderToPerformThisAction,
|
||||||
|
50101: ThisServerNeedsMoreBoostsToPerformThisAction,
|
||||||
|
50109: TheRequestBodyContainsInvalidJSON,
|
||||||
|
50110: TheProvidedFileIsInvalid,
|
||||||
|
50123: TheProvidedFileTypeIsInvalid,
|
||||||
|
50124: TheProvidedFileDurationExceedsMaximumOf52Seconds,
|
||||||
|
50131: OwnerCannotBePendingMember,
|
||||||
|
50132: OwnershipCannotBeTransferredToABotUser,
|
||||||
|
50138: FailedToResizeAssetBelowTheMaximumSize,
|
||||||
|
50144: CannotMixSubscriptionAndNonSubscriptionRolesForAnEmoji,
|
||||||
|
50145: CannotConvertBetweenPremiumEmojiAndNormalEmoji,
|
||||||
|
50146: UploadedFileNotFound,
|
||||||
|
50151: TheSpecifiedEmojiIsInvalid,
|
||||||
|
50159: VoiceMessagesDoNotSupportAdditionalContent,
|
||||||
|
50160: VoiceMessagesMustHaveASingleAudioAttachment,
|
||||||
|
50161: VoiceMessagesMustHaveSupportingMetadata,
|
||||||
|
50162: VoiceMessagesCannotBeEdited,
|
||||||
|
50163: CannotDeleteGuildSubscriptionIntegration,
|
||||||
|
50173: YouCannotSendVoiceMessagesInThisChannel,
|
||||||
|
50178: TheUserAccountMustFirstBeVerified,
|
||||||
|
50192: TheProvidedFileDoesNotHaveAValidDuration,
|
||||||
|
50600: YouDoNotHavePermissionToSendThisSticker,
|
||||||
|
60003: TwoFactorIsRequiredForThisOperation,
|
||||||
|
80004: NoUsersWithDiscordTagExist,
|
||||||
|
90001: ReactionWasBlocked,
|
||||||
|
90002: UserCannotUseBurstReactions,
|
||||||
|
110001: ApplicationNotYetAvailable,
|
||||||
|
130000: APIResourceIsCurrentlyOverloaded,
|
||||||
|
150006: TheStageIsAlreadyOpen,
|
||||||
|
160002: CannotReplyWithoutPermissionToReadMessageHistory,
|
||||||
|
160004: AThreadHasAlreadyBeenCreatedForThisMessage,
|
||||||
|
160005: ThreadIsLocked,
|
||||||
|
160006: MaximumNumberOfActiveThreadsReached,
|
||||||
|
160007: MaximumNumberOfActiveAnnouncementThreadsReached,
|
||||||
|
170001: InvalidJSONForUploadedLottieFile,
|
||||||
|
170002: UploadedLottiesCannotContainRasterizedImages,
|
||||||
|
170003: StickerMaximumFramerateExceeded,
|
||||||
|
170004: StickerFrameCountExceedsMaximumOf1000Frames,
|
||||||
|
170005: LottieAnimationMaximumDimensionsExceeded,
|
||||||
|
170006: StickerFrameRateIsEitherTooSmallOrTooLarge,
|
||||||
|
170007: StickerAnimationDurationExceedsMaximumOf5Seconds,
|
||||||
|
180000: CannotUpdateAFinishedEvent,
|
||||||
|
180002: FailedToCreateStageNeededForStageEvent,
|
||||||
|
200000: MessageWasBlockedByAutomaticModeration,
|
||||||
|
200001: TitleWasBlockedByAutomaticModeration,
|
||||||
|
220001: WebhooksPostedToForumChannelsMustHaveAThreadNameOrThreadId,
|
||||||
|
220002: WebhooksPostedToForumChannelsCannotHaveBothAThreadNameAndThreadId,
|
||||||
|
220003: WebhooksCanOnlyCreateThreadsInForumChannels,
|
||||||
|
220004: WebhookServicesCannotBeUsedInForumChannels,
|
||||||
|
240000: MessageBlockedByHarmfulLinksFilter,
|
||||||
|
350000: CannotEnableOnboardingRequirementsAreNotMet,
|
||||||
|
350001: CannotUpdateOnboardingWhileBelowRequirements,
|
||||||
|
500000: FailedToBanUsers,
|
||||||
|
520000: PollVotingBlocked,
|
||||||
|
520001: PollExpired,
|
||||||
|
520002: InvalidChannelTypeForPollCreation,
|
||||||
|
520003: CannotEditAPollMessage,
|
||||||
|
520004: CannotUseAnEmojiIncludedWithThePoll,
|
||||||
|
520006: CannotExpireANonPollMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class HTTPClient:
|
class HTTPClient:
|
||||||
"""Handles HTTP requests to the Discord API."""
|
"""Handles HTTP requests to the Discord API."""
|
||||||
@ -208,6 +429,17 @@ class HTTPClient:
|
|||||||
discord_error_code = (
|
discord_error_code = (
|
||||||
data.get("code") if isinstance(data, dict) else None
|
data.get("code") if isinstance(data, dict) else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if discord_error_code in DISCORD_ERROR_CODE_TO_EXCEPTION:
|
||||||
|
exc_class = DISCORD_ERROR_CODE_TO_EXCEPTION[discord_error_code]
|
||||||
|
raise exc_class(
|
||||||
|
response,
|
||||||
|
f"API Error on {method} {endpoint}: {error_text}",
|
||||||
|
status=response.status,
|
||||||
|
text=error_text,
|
||||||
|
error_code=discord_error_code,
|
||||||
|
)
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
response,
|
response,
|
||||||
f"API Error on {method} {endpoint}: {error_text}",
|
f"API Error on {method} {endpoint}: {error_text}",
|
||||||
|
@ -1,12 +1,91 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
import aiohttp
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
from disagreement.errors import (
|
from disagreement.errors import (
|
||||||
HTTPException,
|
HTTPException,
|
||||||
RateLimitError,
|
RateLimitError,
|
||||||
AppCommandOptionConversionError,
|
AppCommandOptionConversionError,
|
||||||
Forbidden,
|
Forbidden,
|
||||||
NotFound,
|
NotFound,
|
||||||
|
UnknownAccount,
|
||||||
|
MaximumNumberOfGuildsReached,
|
||||||
)
|
)
|
||||||
|
from disagreement.http import HTTPClient
|
||||||
|
|
||||||
|
|
||||||
|
# A fixture to provide an HTTPClient with a mocked session
|
||||||
|
@pytest.fixture
|
||||||
|
def http_client():
|
||||||
|
# Using a real session and patching the request method is more robust
|
||||||
|
client = HTTPClient(token="fake_token")
|
||||||
|
yield client
|
||||||
|
# Cleanup: close the session after the test
|
||||||
|
# This requires making the fixture async or running this in an event loop
|
||||||
|
async def close_session():
|
||||||
|
if client._session:
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
loop.run_until_complete(close_session())
|
||||||
|
except RuntimeError:
|
||||||
|
asyncio.run(close_session())
|
||||||
|
|
||||||
|
|
||||||
|
# Mock aiohttp response
|
||||||
|
class MockAiohttpResponse:
|
||||||
|
def __init__(self, status, json_data, headers=None):
|
||||||
|
self.status = status
|
||||||
|
self._json_data = json_data
|
||||||
|
self.headers = headers or {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
return self._json_data
|
||||||
|
|
||||||
|
async def text(self):
|
||||||
|
return str(self._json_data)
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"error_code, error_message, expected_exception",
|
||||||
|
[
|
||||||
|
(10001, "Unknown account", UnknownAccount),
|
||||||
|
(30001, "Maximum number of guilds reached", MaximumNumberOfGuildsReached),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_error_code_mapping_raises_correct_exception(
|
||||||
|
http_client, error_code, error_message, expected_exception
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Tests if the HTTP client correctly raises a specific exception
|
||||||
|
based on the Discord error code.
|
||||||
|
"""
|
||||||
|
mock_response = MockAiohttpResponse(
|
||||||
|
status=400, json_data={"code": error_code, "message": error_message}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Patch the session object to control the response
|
||||||
|
with patch("aiohttp.ClientSession") as mock_session_class:
|
||||||
|
mock_session_instance = mock_session_class.return_value
|
||||||
|
mock_session_instance.request.return_value = mock_response
|
||||||
|
|
||||||
|
# Assert that the correct exception is raised
|
||||||
|
with pytest.raises(expected_exception) as excinfo:
|
||||||
|
await http_client.request("GET", "/test-endpoint")
|
||||||
|
|
||||||
|
# Optionally, check the exception details
|
||||||
|
assert excinfo.value.status == 400
|
||||||
|
assert excinfo.value.error_code == error_code
|
||||||
|
assert error_message in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
def test_http_exception_message():
|
def test_http_exception_message():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user