Implements specific exception classes for all Discord API error codes to provide more granular error handling and better debugging experience. Creates individual exception classes for each Discord error code (10001-520006) with descriptive names and documentation strings that include the official error messages and codes. Updates HTTP client to automatically map Discord error codes to their corresponding exception classes, allowing developers to catch specific error types rather than generic HTTP exceptions. Includes comprehensive test coverage for the new error code mapping functionality to ensure correct exception raising based on API responses.
1353 lines
50 KiB
Python
1353 lines
50 KiB
Python
# disagreement/http.py
|
|
|
|
"""
|
|
HTTP client for interacting with the Discord REST API.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
import aiohttp # pylint: disable=import-error
|
|
import json
|
|
from urllib.parse import quote
|
|
from typing import Optional, Dict, Any, Union, TYPE_CHECKING, List
|
|
|
|
from .errors import * # Import all custom exceptions
|
|
from . import __version__ # For User-Agent
|
|
from .rate_limiter import RateLimiter
|
|
from .interactions import InteractionResponsePayload
|
|
|
|
if TYPE_CHECKING:
|
|
from .client import Client
|
|
from .models import Message, Webhook, File, StageInstance, Invite
|
|
from .interactions import ApplicationCommand, Snowflake
|
|
|
|
# Discord API constants
|
|
API_BASE_URL = "https://discord.com/api/v10" # Using API v10
|
|
|
|
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:
|
|
"""Handles HTTP requests to the Discord API."""
|
|
|
|
def __init__(
|
|
self,
|
|
token: str,
|
|
client_session: Optional[aiohttp.ClientSession] = None,
|
|
verbose: bool = False,
|
|
**session_kwargs: Any,
|
|
):
|
|
"""Create a new HTTP client.
|
|
|
|
Parameters
|
|
----------
|
|
token:
|
|
Bot token for authentication.
|
|
client_session:
|
|
Optional existing :class:`aiohttp.ClientSession`.
|
|
verbose:
|
|
If ``True``, log HTTP requests and responses.
|
|
**session_kwargs:
|
|
Additional options forwarded to :class:`aiohttp.ClientSession`, such
|
|
as ``proxy`` or ``connector``.
|
|
"""
|
|
|
|
self.token = token
|
|
self._session: Optional[aiohttp.ClientSession] = client_session
|
|
self._session_kwargs: Dict[str, Any] = session_kwargs
|
|
self.user_agent = f"DiscordBot (https://github.com/Slipstreamm/disagreement, {__version__})" # Customize URL
|
|
|
|
self.verbose = verbose
|
|
|
|
self._rate_limiter = RateLimiter()
|
|
|
|
async def _ensure_session(self):
|
|
if self._session is None or self._session.closed:
|
|
self._session = aiohttp.ClientSession(**self._session_kwargs)
|
|
|
|
async def close(self):
|
|
"""Closes the underlying aiohttp.ClientSession."""
|
|
if self._session and not self._session.closed:
|
|
await self._session.close()
|
|
|
|
async def request(
|
|
self,
|
|
method: str,
|
|
endpoint: str,
|
|
payload: Optional[
|
|
Union[Dict[str, Any], List[Dict[str, Any]], aiohttp.FormData]
|
|
] = None,
|
|
params: Optional[Dict[str, Any]] = None,
|
|
is_json: bool = True,
|
|
use_auth_header: bool = True,
|
|
custom_headers: Optional[Dict[str, str]] = None,
|
|
) -> Any:
|
|
"""Makes an HTTP request to the Discord API."""
|
|
await self._ensure_session()
|
|
|
|
url = f"{API_BASE_URL}{endpoint}"
|
|
final_headers: Dict[str, str] = { # Renamed to final_headers
|
|
"User-Agent": self.user_agent,
|
|
}
|
|
if use_auth_header:
|
|
final_headers["Authorization"] = f"Bot {self.token}"
|
|
|
|
if is_json and payload:
|
|
final_headers["Content-Type"] = "application/json"
|
|
|
|
if custom_headers: # Merge custom headers
|
|
final_headers.update(custom_headers)
|
|
|
|
if self.verbose:
|
|
logger.debug(
|
|
"HTTP REQUEST: %s %s | payload=%s params=%s",
|
|
method,
|
|
url,
|
|
payload,
|
|
params,
|
|
)
|
|
|
|
route = f"{method.upper()}:{endpoint}"
|
|
|
|
for attempt in range(5): # Max 5 retries for rate limits
|
|
await self._rate_limiter.acquire(route)
|
|
assert self._session is not None, "ClientSession not initialized"
|
|
async with self._session.request(
|
|
method,
|
|
url,
|
|
json=payload if is_json else None,
|
|
data=payload if not is_json else None,
|
|
headers=final_headers,
|
|
params=params,
|
|
) as response:
|
|
|
|
data = None
|
|
try:
|
|
if response.headers.get("Content-Type", "").startswith(
|
|
"application/json"
|
|
):
|
|
data = await response.json()
|
|
else:
|
|
# For non-JSON responses, like fetching images or other files
|
|
# We might return the raw response or handle it differently
|
|
# For now, let's assume most API calls expect JSON
|
|
data = await response.text()
|
|
except (aiohttp.ContentTypeError, json.JSONDecodeError):
|
|
data = (
|
|
await response.text()
|
|
) # Fallback to text if JSON parsing fails
|
|
|
|
if self.verbose:
|
|
logger.debug(
|
|
"HTTP RESPONSE: %s %s | %s", response.status, url, data
|
|
)
|
|
|
|
self._rate_limiter.release(route, response.headers)
|
|
|
|
if 200 <= response.status < 300:
|
|
if response.status == 204:
|
|
return None
|
|
return data
|
|
|
|
# Rate limit handling
|
|
if response.status == 429: # Rate limited
|
|
retry_after_str = response.headers.get("Retry-After", "1")
|
|
try:
|
|
retry_after = float(retry_after_str)
|
|
except ValueError:
|
|
retry_after = 1.0 # Default retry if header is malformed
|
|
|
|
is_global = (
|
|
response.headers.get("X-RateLimit-Global", "false").lower()
|
|
== "true"
|
|
)
|
|
|
|
error_message = f"Rate limited on {method} {endpoint}."
|
|
if data and isinstance(data, dict) and "message" in data:
|
|
error_message += f" Discord says: {data['message']}"
|
|
|
|
await self._rate_limiter.handle_rate_limit(
|
|
route, retry_after, is_global
|
|
)
|
|
|
|
if attempt < 4: # Don't log on the last attempt before raising
|
|
logger.warning(
|
|
"%s Retrying after %ss (Attempt %s/5). Global: %s",
|
|
error_message,
|
|
retry_after,
|
|
attempt + 1,
|
|
is_global,
|
|
)
|
|
continue # Retry the request
|
|
else: # Last attempt failed
|
|
raise RateLimitError(
|
|
response,
|
|
message=error_message,
|
|
retry_after=retry_after,
|
|
is_global=is_global,
|
|
)
|
|
|
|
# Other error handling
|
|
if response.status == 401: # Unauthorized
|
|
raise AuthenticationError(response, "Invalid token provided.")
|
|
if response.status == 403: # Forbidden
|
|
raise HTTPException(
|
|
response,
|
|
"Missing permissions or access denied.",
|
|
status=response.status,
|
|
text=str(data),
|
|
)
|
|
|
|
# General HTTP error
|
|
error_text = str(data) if data else "Unknown error"
|
|
discord_error_code = (
|
|
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(
|
|
response,
|
|
f"API Error on {method} {endpoint}: {error_text}",
|
|
status=response.status,
|
|
text=error_text,
|
|
error_code=discord_error_code,
|
|
)
|
|
|
|
# Should not be reached if retries are exhausted by RateLimitError
|
|
raise DisagreementException(
|
|
f"Failed request to {method} {endpoint} after multiple retries."
|
|
)
|
|
|
|
# --- Specific API call methods ---
|
|
|
|
async def get_gateway_bot(self) -> Dict[str, Any]:
|
|
"""Gets the WSS URL and sharding information for the Gateway."""
|
|
return await self.request("GET", "/gateway/bot")
|
|
|
|
async def send_message(
|
|
self,
|
|
channel_id: str,
|
|
content: Optional[str] = None,
|
|
tts: bool = False,
|
|
embeds: Optional[List[Dict[str, Any]]] = None,
|
|
components: Optional[List[Dict[str, Any]]] = None,
|
|
allowed_mentions: Optional[dict] = None,
|
|
message_reference: Optional[Dict[str, Any]] = None,
|
|
attachments: Optional[List[Any]] = None,
|
|
files: Optional[List[Any]] = None,
|
|
flags: Optional[int] = None,
|
|
) -> Dict[str, Any]:
|
|
"""Sends a message to a channel.
|
|
|
|
Parameters
|
|
----------
|
|
attachments:
|
|
A list of attachment payloads to include with the message.
|
|
files:
|
|
A list of :class:`File` objects containing binary data to upload.
|
|
|
|
Returns
|
|
-------
|
|
Dict[str, Any]
|
|
The created message data.
|
|
"""
|
|
payload: Dict[str, Any] = {}
|
|
if content is not None: # Content is optional if embeds/components are present
|
|
payload["content"] = content
|
|
if tts:
|
|
payload["tts"] = True
|
|
if embeds:
|
|
payload["embeds"] = embeds
|
|
if components:
|
|
payload["components"] = components
|
|
if allowed_mentions:
|
|
payload["allowed_mentions"] = allowed_mentions
|
|
all_files: List["File"] = []
|
|
if attachments is not None:
|
|
payload["attachments"] = []
|
|
for a in attachments:
|
|
if hasattr(a, "data") and hasattr(a, "filename"):
|
|
idx = len(all_files)
|
|
all_files.append(a)
|
|
payload["attachments"].append({"id": idx, "filename": a.filename})
|
|
else:
|
|
payload["attachments"].append(
|
|
a.to_dict() if hasattr(a, "to_dict") else a
|
|
)
|
|
if files is not None:
|
|
for f in files:
|
|
if hasattr(f, "data") and hasattr(f, "filename"):
|
|
idx = len(all_files)
|
|
all_files.append(f)
|
|
if "attachments" not in payload:
|
|
payload["attachments"] = []
|
|
payload["attachments"].append({"id": idx, "filename": f.filename})
|
|
else:
|
|
raise TypeError("files must be File objects")
|
|
if flags:
|
|
payload["flags"] = flags
|
|
if message_reference:
|
|
payload["message_reference"] = message_reference
|
|
|
|
if not payload:
|
|
raise ValueError("Message must have content, embeds, or components.")
|
|
|
|
if all_files:
|
|
form = aiohttp.FormData()
|
|
form.add_field(
|
|
"payload_json", json.dumps(payload), content_type="application/json"
|
|
)
|
|
for idx, f in enumerate(all_files):
|
|
form.add_field(
|
|
f"files[{idx}]",
|
|
f.data,
|
|
filename=f.filename,
|
|
content_type="application/octet-stream",
|
|
)
|
|
return await self.request(
|
|
"POST",
|
|
f"/channels/{channel_id}/messages",
|
|
payload=form,
|
|
is_json=False,
|
|
)
|
|
|
|
return await self.request(
|
|
"POST", f"/channels/{channel_id}/messages", payload=payload
|
|
)
|
|
|
|
async def edit_message(
|
|
self,
|
|
channel_id: str,
|
|
message_id: str,
|
|
payload: Dict[str, Any],
|
|
) -> Dict[str, Any]:
|
|
"""Edits a message in a channel."""
|
|
|
|
return await self.request(
|
|
"PATCH",
|
|
f"/channels/{channel_id}/messages/{message_id}",
|
|
payload=payload,
|
|
)
|
|
|
|
async def get_message(
|
|
self, channel_id: "Snowflake", message_id: "Snowflake"
|
|
) -> Dict[str, Any]:
|
|
"""Fetches a message from a channel."""
|
|
|
|
return await self.request(
|
|
"GET", f"/channels/{channel_id}/messages/{message_id}"
|
|
)
|
|
|
|
async def delete_message(
|
|
self, channel_id: "Snowflake", message_id: "Snowflake"
|
|
) -> None:
|
|
"""Deletes a message in a channel."""
|
|
|
|
await self.request("DELETE", f"/channels/{channel_id}/messages/{message_id}")
|
|
|
|
async def create_reaction(
|
|
self, channel_id: "Snowflake", message_id: "Snowflake", emoji: str
|
|
) -> None:
|
|
"""Adds a reaction to a message as the current user."""
|
|
encoded = quote(emoji)
|
|
await self.request(
|
|
"PUT",
|
|
f"/channels/{channel_id}/messages/{message_id}/reactions/{encoded}/@me",
|
|
)
|
|
|
|
async def delete_reaction(
|
|
self, channel_id: "Snowflake", message_id: "Snowflake", emoji: str
|
|
) -> None:
|
|
"""Removes the current user's reaction from a message."""
|
|
encoded = quote(emoji)
|
|
await self.request(
|
|
"DELETE",
|
|
f"/channels/{channel_id}/messages/{message_id}/reactions/{encoded}/@me",
|
|
)
|
|
|
|
async def delete_user_reaction(
|
|
self,
|
|
channel_id: "Snowflake",
|
|
message_id: "Snowflake",
|
|
emoji: str,
|
|
user_id: "Snowflake",
|
|
) -> None:
|
|
"""Removes another user's reaction from a message."""
|
|
encoded = quote(emoji)
|
|
await self.request(
|
|
"DELETE",
|
|
f"/channels/{channel_id}/messages/{message_id}/reactions/{encoded}/{user_id}",
|
|
)
|
|
|
|
async def get_reactions(
|
|
self, channel_id: "Snowflake", message_id: "Snowflake", emoji: str
|
|
) -> List[Dict[str, Any]]:
|
|
"""Fetches the users that reacted with a specific emoji."""
|
|
encoded = quote(emoji)
|
|
return await self.request(
|
|
"GET",
|
|
f"/channels/{channel_id}/messages/{message_id}/reactions/{encoded}",
|
|
)
|
|
|
|
async def clear_reactions(
|
|
self, channel_id: "Snowflake", message_id: "Snowflake"
|
|
) -> None:
|
|
"""Removes all reactions from a message."""
|
|
|
|
await self.request(
|
|
"DELETE",
|
|
f"/channels/{channel_id}/messages/{message_id}/reactions",
|
|
)
|
|
|
|
async def bulk_delete_messages(
|
|
self, channel_id: "Snowflake", messages: List["Snowflake"]
|
|
) -> List["Snowflake"]:
|
|
"""Bulk deletes messages in a channel and returns their IDs."""
|
|
|
|
await self.request(
|
|
"POST",
|
|
f"/channels/{channel_id}/messages/bulk-delete",
|
|
payload={"messages": messages},
|
|
)
|
|
return messages
|
|
|
|
async def get_pinned_messages(
|
|
self, channel_id: "Snowflake"
|
|
) -> List[Dict[str, Any]]:
|
|
"""Fetches all pinned messages in a channel."""
|
|
|
|
return await self.request("GET", f"/channels/{channel_id}/pins")
|
|
|
|
async def pin_message(
|
|
self, channel_id: "Snowflake", message_id: "Snowflake"
|
|
) -> None:
|
|
"""Pins a message in a channel."""
|
|
|
|
await self.request("PUT", f"/channels/{channel_id}/pins/{message_id}")
|
|
|
|
async def unpin_message(
|
|
self, channel_id: "Snowflake", message_id: "Snowflake"
|
|
) -> None:
|
|
"""Unpins a message from a channel."""
|
|
|
|
await self.request("DELETE", f"/channels/{channel_id}/pins/{message_id}")
|
|
|
|
async def delete_channel(
|
|
self, channel_id: str, reason: Optional[str] = None
|
|
) -> None:
|
|
"""Deletes a channel.
|
|
|
|
If the channel is a guild channel, requires the MANAGE_CHANNELS permission.
|
|
If the channel is a thread, requires the MANAGE_THREADS permission (if locked) or
|
|
be the thread creator (if not locked).
|
|
Deleting a category does not delete its child channels.
|
|
"""
|
|
custom_headers = {}
|
|
if reason:
|
|
custom_headers["X-Audit-Log-Reason"] = reason
|
|
|
|
await self.request(
|
|
"DELETE",
|
|
f"/channels/{channel_id}",
|
|
custom_headers=custom_headers if custom_headers else None,
|
|
)
|
|
|
|
async def edit_channel(
|
|
self,
|
|
channel_id: "Snowflake",
|
|
payload: Dict[str, Any],
|
|
reason: Optional[str] = None,
|
|
) -> Dict[str, Any]:
|
|
"""Edits a channel."""
|
|
headers = {"X-Audit-Log-Reason": reason} if reason else None
|
|
return await self.request(
|
|
"PATCH",
|
|
f"/channels/{channel_id}",
|
|
payload=payload,
|
|
custom_headers=headers,
|
|
)
|
|
|
|
async def get_channel(self, channel_id: str) -> Dict[str, Any]:
|
|
"""Fetches a channel by ID."""
|
|
return await self.request("GET", f"/channels/{channel_id}")
|
|
|
|
async def get_channel_invites(
|
|
self, channel_id: "Snowflake"
|
|
) -> List[Dict[str, Any]]:
|
|
"""Fetches the invites for a channel."""
|
|
|
|
return await self.request("GET", f"/channels/{channel_id}/invites")
|
|
|
|
async def create_invite(
|
|
self, channel_id: "Snowflake", payload: Dict[str, Any]
|
|
) -> "Invite":
|
|
"""Creates an invite for a channel."""
|
|
|
|
data = await self.request(
|
|
"POST", f"/channels/{channel_id}/invites", payload=payload
|
|
)
|
|
from .models import Invite
|
|
|
|
return Invite.from_dict(data)
|
|
|
|
async def delete_invite(self, code: str) -> None:
|
|
"""Deletes an invite by code."""
|
|
|
|
await self.request("DELETE", f"/invites/{code}")
|
|
|
|
async def create_webhook(
|
|
self, channel_id: "Snowflake", payload: Dict[str, Any]
|
|
) -> "Webhook":
|
|
"""Creates a webhook in the specified channel."""
|
|
|
|
data = await self.request(
|
|
"POST", f"/channels/{channel_id}/webhooks", payload=payload
|
|
)
|
|
from .models import Webhook
|
|
|
|
return Webhook(data)
|
|
|
|
async def edit_webhook(
|
|
self, webhook_id: "Snowflake", payload: Dict[str, Any]
|
|
) -> "Webhook":
|
|
"""Edits an existing webhook."""
|
|
|
|
data = await self.request("PATCH", f"/webhooks/{webhook_id}", payload=payload)
|
|
from .models import Webhook
|
|
|
|
return Webhook(data)
|
|
|
|
async def delete_webhook(self, webhook_id: "Snowflake") -> None:
|
|
"""Deletes a webhook."""
|
|
|
|
await self.request("DELETE", f"/webhooks/{webhook_id}")
|
|
|
|
async def execute_webhook(
|
|
self,
|
|
webhook_id: "Snowflake",
|
|
token: str,
|
|
*,
|
|
content: Optional[str] = None,
|
|
tts: bool = False,
|
|
embeds: Optional[List[Dict[str, Any]]] = None,
|
|
components: Optional[List[Dict[str, Any]]] = None,
|
|
allowed_mentions: Optional[dict] = None,
|
|
attachments: Optional[List[Any]] = None,
|
|
files: Optional[List[Any]] = None,
|
|
flags: Optional[int] = None,
|
|
username: Optional[str] = None,
|
|
avatar_url: Optional[str] = None,
|
|
) -> Dict[str, Any]:
|
|
"""Executes a webhook and returns the created message."""
|
|
|
|
payload: Dict[str, Any] = {}
|
|
if content is not None:
|
|
payload["content"] = content
|
|
if tts:
|
|
payload["tts"] = True
|
|
if embeds:
|
|
payload["embeds"] = embeds
|
|
if components:
|
|
payload["components"] = components
|
|
if allowed_mentions:
|
|
payload["allowed_mentions"] = allowed_mentions
|
|
if username:
|
|
payload["username"] = username
|
|
if avatar_url:
|
|
payload["avatar_url"] = avatar_url
|
|
|
|
all_files: List["File"] = []
|
|
if attachments is not None:
|
|
payload["attachments"] = []
|
|
for a in attachments:
|
|
if hasattr(a, "data") and hasattr(a, "filename"):
|
|
idx = len(all_files)
|
|
all_files.append(a)
|
|
payload["attachments"].append({"id": idx, "filename": a.filename})
|
|
else:
|
|
payload["attachments"].append(
|
|
a.to_dict() if hasattr(a, "to_dict") else a
|
|
)
|
|
if files is not None:
|
|
for f in files:
|
|
if hasattr(f, "data") and hasattr(f, "filename"):
|
|
idx = len(all_files)
|
|
all_files.append(f)
|
|
if "attachments" not in payload:
|
|
payload["attachments"] = []
|
|
payload["attachments"].append({"id": idx, "filename": f.filename})
|
|
else:
|
|
raise TypeError("files must be File objects")
|
|
if flags:
|
|
payload["flags"] = flags
|
|
|
|
if all_files:
|
|
form = aiohttp.FormData()
|
|
form.add_field(
|
|
"payload_json", json.dumps(payload), content_type="application/json"
|
|
)
|
|
for idx, f in enumerate(all_files):
|
|
form.add_field(
|
|
f"files[{idx}]",
|
|
f.data,
|
|
filename=f.filename,
|
|
content_type="application/octet-stream",
|
|
)
|
|
return await self.request(
|
|
"POST",
|
|
f"/webhooks/{webhook_id}/{token}",
|
|
payload=form,
|
|
is_json=False,
|
|
use_auth_header=False,
|
|
)
|
|
|
|
return await self.request(
|
|
"POST",
|
|
f"/webhooks/{webhook_id}/{token}",
|
|
payload=payload,
|
|
use_auth_header=False,
|
|
)
|
|
|
|
async def get_user(self, user_id: "Snowflake") -> Dict[str, Any]:
|
|
"""Fetches a user object for a given user ID."""
|
|
return await self.request("GET", f"/users/{user_id}")
|
|
|
|
async def get_guild_member(
|
|
self, guild_id: "Snowflake", user_id: "Snowflake"
|
|
) -> Dict[str, Any]:
|
|
"""Returns a guild member object for the specified user."""
|
|
return await self.request("GET", f"/guilds/{guild_id}/members/{user_id}")
|
|
|
|
async def kick_member(
|
|
self, guild_id: "Snowflake", user_id: "Snowflake", reason: Optional[str] = None
|
|
) -> None:
|
|
"""Kicks a member from the guild."""
|
|
headers = {"X-Audit-Log-Reason": reason} if reason else None
|
|
await self.request(
|
|
"DELETE",
|
|
f"/guilds/{guild_id}/members/{user_id}",
|
|
custom_headers=headers,
|
|
)
|
|
|
|
async def ban_member(
|
|
self,
|
|
guild_id: "Snowflake",
|
|
user_id: "Snowflake",
|
|
*,
|
|
delete_message_seconds: int = 0,
|
|
reason: Optional[str] = None,
|
|
) -> None:
|
|
"""Bans a member from the guild."""
|
|
payload = {}
|
|
if delete_message_seconds:
|
|
payload["delete_message_seconds"] = delete_message_seconds
|
|
headers = {"X-Audit-Log-Reason": reason} if reason else None
|
|
await self.request(
|
|
"PUT",
|
|
f"/guilds/{guild_id}/bans/{user_id}",
|
|
payload=payload if payload else None,
|
|
custom_headers=headers,
|
|
)
|
|
|
|
async def timeout_member(
|
|
self,
|
|
guild_id: "Snowflake",
|
|
user_id: "Snowflake",
|
|
*,
|
|
until: Optional[str],
|
|
reason: Optional[str] = None,
|
|
) -> Dict[str, Any]:
|
|
"""Times out a member until the given ISO8601 timestamp."""
|
|
payload = {"communication_disabled_until": until}
|
|
headers = {"X-Audit-Log-Reason": reason} if reason else None
|
|
return await self.request(
|
|
"PATCH",
|
|
f"/guilds/{guild_id}/members/{user_id}",
|
|
payload=payload,
|
|
custom_headers=headers,
|
|
)
|
|
|
|
async def get_guild_roles(self, guild_id: "Snowflake") -> List[Dict[str, Any]]:
|
|
"""Returns a list of role objects for the guild."""
|
|
return await self.request("GET", f"/guilds/{guild_id}/roles")
|
|
|
|
async def get_guild(self, guild_id: "Snowflake") -> Dict[str, Any]:
|
|
"""Fetches a guild object for a given guild ID."""
|
|
return await self.request("GET", f"/guilds/{guild_id}")
|
|
|
|
async def get_guild_templates(self, guild_id: "Snowflake") -> List[Dict[str, Any]]:
|
|
"""Fetches all templates for the given guild."""
|
|
return await self.request("GET", f"/guilds/{guild_id}/templates")
|
|
|
|
async def create_guild_template(
|
|
self, guild_id: "Snowflake", payload: Dict[str, Any]
|
|
) -> Dict[str, Any]:
|
|
"""Creates a guild template."""
|
|
return await self.request(
|
|
"POST", f"/guilds/{guild_id}/templates", payload=payload
|
|
)
|
|
|
|
async def sync_guild_template(
|
|
self, guild_id: "Snowflake", template_code: str
|
|
) -> Dict[str, Any]:
|
|
"""Syncs a guild template to the guild's current state."""
|
|
return await self.request(
|
|
"PUT",
|
|
f"/guilds/{guild_id}/templates/{template_code}",
|
|
)
|
|
|
|
async def delete_guild_template(
|
|
self, guild_id: "Snowflake", template_code: str
|
|
) -> None:
|
|
"""Deletes a guild template."""
|
|
await self.request("DELETE", f"/guilds/{guild_id}/templates/{template_code}")
|
|
|
|
async def get_guild_scheduled_events(
|
|
self, guild_id: "Snowflake"
|
|
) -> List[Dict[str, Any]]:
|
|
"""Returns a list of scheduled events for the guild."""
|
|
|
|
return await self.request("GET", f"/guilds/{guild_id}/scheduled-events")
|
|
|
|
async def get_guild_scheduled_event(
|
|
self, guild_id: "Snowflake", event_id: "Snowflake"
|
|
) -> Dict[str, Any]:
|
|
"""Returns a guild scheduled event."""
|
|
|
|
return await self.request(
|
|
"GET", f"/guilds/{guild_id}/scheduled-events/{event_id}"
|
|
)
|
|
|
|
async def create_guild_scheduled_event(
|
|
self, guild_id: "Snowflake", payload: Dict[str, Any]
|
|
) -> Dict[str, Any]:
|
|
"""Creates a guild scheduled event."""
|
|
|
|
return await self.request(
|
|
"POST", f"/guilds/{guild_id}/scheduled-events", payload=payload
|
|
)
|
|
|
|
async def edit_guild_scheduled_event(
|
|
self, guild_id: "Snowflake", event_id: "Snowflake", payload: Dict[str, Any]
|
|
) -> Dict[str, Any]:
|
|
"""Edits a guild scheduled event."""
|
|
|
|
return await self.request(
|
|
"PATCH",
|
|
f"/guilds/{guild_id}/scheduled-events/{event_id}",
|
|
payload=payload,
|
|
)
|
|
|
|
async def delete_guild_scheduled_event(
|
|
self, guild_id: "Snowflake", event_id: "Snowflake"
|
|
) -> None:
|
|
"""Deletes a guild scheduled event."""
|
|
|
|
await self.request("DELETE", f"/guilds/{guild_id}/scheduled-events/{event_id}")
|
|
|
|
async def get_audit_logs(
|
|
self, guild_id: "Snowflake", **filters: Any
|
|
) -> Dict[str, Any]:
|
|
"""Fetches audit log entries for a guild."""
|
|
params = {k: v for k, v in filters.items() if v is not None}
|
|
return await self.request(
|
|
"GET",
|
|
f"/guilds/{guild_id}/audit-logs",
|
|
params=params if params else None,
|
|
)
|
|
|
|
# Add other methods like:
|
|
# async def get_guild(self, guild_id: str) -> Dict[str, Any]: ...
|
|
# async def create_reaction(self, channel_id: str, message_id: str, emoji: str) -> None: ...
|
|
# etc.
|
|
# --- Application Command Endpoints ---
|
|
|
|
# Global Application Commands
|
|
async def get_global_application_commands(
|
|
self, application_id: "Snowflake", with_localizations: bool = False
|
|
) -> List["ApplicationCommand"]:
|
|
"""Fetches all global commands for your application."""
|
|
params = {"with_localizations": str(with_localizations).lower()}
|
|
data = await self.request(
|
|
"GET", f"/applications/{application_id}/commands", params=params
|
|
)
|
|
from .interactions import ApplicationCommand # Ensure constructor is available
|
|
|
|
return [ApplicationCommand(cmd_data) for cmd_data in data]
|
|
|
|
async def create_global_application_command(
|
|
self, application_id: "Snowflake", payload: Dict[str, Any]
|
|
) -> "ApplicationCommand":
|
|
"""Creates a new global command."""
|
|
data = await self.request(
|
|
"POST", f"/applications/{application_id}/commands", payload=payload
|
|
)
|
|
from .interactions import ApplicationCommand
|
|
|
|
return ApplicationCommand(data)
|
|
|
|
async def get_global_application_command(
|
|
self, application_id: "Snowflake", command_id: "Snowflake"
|
|
) -> "ApplicationCommand":
|
|
"""Fetches a specific global command."""
|
|
data = await self.request(
|
|
"GET", f"/applications/{application_id}/commands/{command_id}"
|
|
)
|
|
from .interactions import ApplicationCommand
|
|
|
|
return ApplicationCommand(data)
|
|
|
|
async def edit_global_application_command(
|
|
self,
|
|
application_id: "Snowflake",
|
|
command_id: "Snowflake",
|
|
payload: Dict[str, Any],
|
|
) -> "ApplicationCommand":
|
|
"""Edits a specific global command."""
|
|
data = await self.request(
|
|
"PATCH",
|
|
f"/applications/{application_id}/commands/{command_id}",
|
|
payload=payload,
|
|
)
|
|
from .interactions import ApplicationCommand
|
|
|
|
return ApplicationCommand(data)
|
|
|
|
async def delete_global_application_command(
|
|
self, application_id: "Snowflake", command_id: "Snowflake"
|
|
) -> None:
|
|
"""Deletes a specific global command."""
|
|
await self.request(
|
|
"DELETE", f"/applications/{application_id}/commands/{command_id}"
|
|
)
|
|
|
|
async def bulk_overwrite_global_application_commands(
|
|
self, application_id: "Snowflake", payload: List[Dict[str, Any]]
|
|
) -> List["ApplicationCommand"]:
|
|
"""Bulk overwrites all global commands for your application."""
|
|
data = await self.request(
|
|
"PUT", f"/applications/{application_id}/commands", payload=payload
|
|
)
|
|
from .interactions import ApplicationCommand
|
|
|
|
return [ApplicationCommand(cmd_data) for cmd_data in data]
|
|
|
|
# Guild Application Commands
|
|
async def get_guild_application_commands(
|
|
self,
|
|
application_id: "Snowflake",
|
|
guild_id: "Snowflake",
|
|
with_localizations: bool = False,
|
|
) -> List["ApplicationCommand"]:
|
|
"""Fetches all commands for your application for a specific guild."""
|
|
params = {"with_localizations": str(with_localizations).lower()}
|
|
data = await self.request(
|
|
"GET",
|
|
f"/applications/{application_id}/guilds/{guild_id}/commands",
|
|
params=params,
|
|
)
|
|
from .interactions import ApplicationCommand
|
|
|
|
return [ApplicationCommand(cmd_data) for cmd_data in data]
|
|
|
|
async def create_guild_application_command(
|
|
self,
|
|
application_id: "Snowflake",
|
|
guild_id: "Snowflake",
|
|
payload: Dict[str, Any],
|
|
) -> "ApplicationCommand":
|
|
"""Creates a new guild command."""
|
|
data = await self.request(
|
|
"POST",
|
|
f"/applications/{application_id}/guilds/{guild_id}/commands",
|
|
payload=payload,
|
|
)
|
|
from .interactions import ApplicationCommand
|
|
|
|
return ApplicationCommand(data)
|
|
|
|
async def get_guild_application_command(
|
|
self,
|
|
application_id: "Snowflake",
|
|
guild_id: "Snowflake",
|
|
command_id: "Snowflake",
|
|
) -> "ApplicationCommand":
|
|
"""Fetches a specific guild command."""
|
|
data = await self.request(
|
|
"GET",
|
|
f"/applications/{application_id}/guilds/{guild_id}/commands/{command_id}",
|
|
)
|
|
from .interactions import ApplicationCommand
|
|
|
|
return ApplicationCommand(data)
|
|
|
|
async def edit_guild_application_command(
|
|
self,
|
|
application_id: "Snowflake",
|
|
guild_id: "Snowflake",
|
|
command_id: "Snowflake",
|
|
payload: Dict[str, Any],
|
|
) -> "ApplicationCommand":
|
|
"""Edits a specific guild command."""
|
|
data = await self.request(
|
|
"PATCH",
|
|
f"/applications/{application_id}/guilds/{guild_id}/commands/{command_id}",
|
|
payload=payload,
|
|
)
|
|
from .interactions import ApplicationCommand
|
|
|
|
return ApplicationCommand(data)
|
|
|
|
async def delete_guild_application_command(
|
|
self,
|
|
application_id: "Snowflake",
|
|
guild_id: "Snowflake",
|
|
command_id: "Snowflake",
|
|
) -> None:
|
|
"""Deletes a specific guild command."""
|
|
await self.request(
|
|
"DELETE",
|
|
f"/applications/{application_id}/guilds/{guild_id}/commands/{command_id}",
|
|
)
|
|
|
|
async def bulk_overwrite_guild_application_commands(
|
|
self,
|
|
application_id: "Snowflake",
|
|
guild_id: "Snowflake",
|
|
payload: List[Dict[str, Any]],
|
|
) -> List["ApplicationCommand"]:
|
|
"""Bulk overwrites all commands for your application for a specific guild."""
|
|
data = await self.request(
|
|
"PUT",
|
|
f"/applications/{application_id}/guilds/{guild_id}/commands",
|
|
payload=payload,
|
|
)
|
|
from .interactions import ApplicationCommand
|
|
|
|
return [ApplicationCommand(cmd_data) for cmd_data in data]
|
|
|
|
# --- Interaction Response Endpoints ---
|
|
# Note: These methods return Dict[str, Any] representing the Message data.
|
|
# The caller (e.g., AppCommandHandler) will be responsible for constructing Message models
|
|
# if needed, as Message model instantiation requires a `client_instance`.
|
|
|
|
async def create_interaction_response(
|
|
self,
|
|
interaction_id: "Snowflake",
|
|
interaction_token: str,
|
|
payload: Union["InteractionResponsePayload", Dict[str, Any]],
|
|
*,
|
|
ephemeral: bool = False,
|
|
) -> None:
|
|
"""Creates a response to an Interaction.
|
|
|
|
Parameters
|
|
----------
|
|
ephemeral: bool
|
|
Ignored parameter for test compatibility.
|
|
"""
|
|
# Interaction responses do not use the bot token in the Authorization header.
|
|
# They are authenticated by the interaction_token in the URL.
|
|
payload_data: Dict[str, Any]
|
|
if isinstance(payload, InteractionResponsePayload):
|
|
payload_data = payload.to_dict()
|
|
else:
|
|
payload_data = payload
|
|
|
|
await self.request(
|
|
"POST",
|
|
f"/interactions/{interaction_id}/{interaction_token}/callback",
|
|
payload=payload_data,
|
|
use_auth_header=False,
|
|
)
|
|
|
|
async def get_original_interaction_response(
|
|
self, application_id: "Snowflake", interaction_token: str
|
|
) -> Dict[str, Any]:
|
|
"""Gets the initial Interaction response."""
|
|
# This endpoint uses the bot token for auth.
|
|
return await self.request(
|
|
"GET", f"/webhooks/{application_id}/{interaction_token}/messages/@original"
|
|
)
|
|
|
|
async def edit_original_interaction_response(
|
|
self,
|
|
application_id: "Snowflake",
|
|
interaction_token: str,
|
|
payload: Dict[str, Any],
|
|
) -> Dict[str, Any]:
|
|
"""Edits the initial Interaction response."""
|
|
return await self.request(
|
|
"PATCH",
|
|
f"/webhooks/{application_id}/{interaction_token}/messages/@original",
|
|
payload=payload,
|
|
use_auth_header=False,
|
|
) # Docs imply webhook-style auth
|
|
|
|
async def delete_original_interaction_response(
|
|
self, application_id: "Snowflake", interaction_token: str
|
|
) -> None:
|
|
"""Deletes the initial Interaction response."""
|
|
await self.request(
|
|
"DELETE",
|
|
f"/webhooks/{application_id}/{interaction_token}/messages/@original",
|
|
use_auth_header=False,
|
|
) # Docs imply webhook-style auth
|
|
|
|
async def create_followup_message(
|
|
self,
|
|
application_id: "Snowflake",
|
|
interaction_token: str,
|
|
payload: Dict[str, Any],
|
|
) -> Dict[str, Any]:
|
|
"""Creates a followup message for an Interaction."""
|
|
# Followup messages are sent to a webhook endpoint.
|
|
return await self.request(
|
|
"POST",
|
|
f"/webhooks/{application_id}/{interaction_token}",
|
|
payload=payload,
|
|
use_auth_header=False,
|
|
) # Docs imply webhook-style auth
|
|
|
|
async def edit_followup_message(
|
|
self,
|
|
application_id: "Snowflake",
|
|
interaction_token: str,
|
|
message_id: "Snowflake",
|
|
payload: Dict[str, Any],
|
|
) -> Dict[str, Any]:
|
|
"""Edits a followup message for an Interaction."""
|
|
return await self.request(
|
|
"PATCH",
|
|
f"/webhooks/{application_id}/{interaction_token}/messages/{message_id}",
|
|
payload=payload,
|
|
use_auth_header=False,
|
|
) # Docs imply webhook-style auth
|
|
|
|
async def delete_followup_message(
|
|
self,
|
|
application_id: "Snowflake",
|
|
interaction_token: str,
|
|
message_id: "Snowflake",
|
|
) -> None:
|
|
"""Deletes a followup message for an Interaction."""
|
|
await self.request(
|
|
"DELETE",
|
|
f"/webhooks/{application_id}/{interaction_token}/messages/{message_id}",
|
|
use_auth_header=False,
|
|
)
|
|
|
|
async def trigger_typing(self, channel_id: str) -> None:
|
|
"""Sends a typing indicator to the specified channel."""
|
|
await self.request("POST", f"/channels/{channel_id}/typing")
|
|
|
|
async def start_stage_instance(
|
|
self, payload: Dict[str, Any], reason: Optional[str] = None
|
|
) -> "StageInstance":
|
|
"""Starts a stage instance."""
|
|
|
|
headers = {"X-Audit-Log-Reason": reason} if reason else None
|
|
data = await self.request(
|
|
"POST", "/stage-instances", payload=payload, custom_headers=headers
|
|
)
|
|
from .models import StageInstance
|
|
|
|
return StageInstance(data)
|
|
|
|
async def edit_stage_instance(
|
|
self,
|
|
channel_id: "Snowflake",
|
|
payload: Dict[str, Any],
|
|
reason: Optional[str] = None,
|
|
) -> "StageInstance":
|
|
"""Edits an existing stage instance."""
|
|
|
|
headers = {"X-Audit-Log-Reason": reason} if reason else None
|
|
data = await self.request(
|
|
"PATCH",
|
|
f"/stage-instances/{channel_id}",
|
|
payload=payload,
|
|
custom_headers=headers,
|
|
)
|
|
from .models import StageInstance
|
|
|
|
return StageInstance(data)
|
|
|
|
async def end_stage_instance(
|
|
self, channel_id: "Snowflake", reason: Optional[str] = None
|
|
) -> None:
|
|
"""Ends a stage instance."""
|
|
|
|
headers = {"X-Audit-Log-Reason": reason} if reason else None
|
|
await self.request(
|
|
"DELETE", f"/stage-instances/{channel_id}", custom_headers=headers
|
|
)
|
|
|
|
async def get_voice_regions(self) -> List[Dict[str, Any]]:
|
|
"""Returns available voice regions."""
|
|
return await self.request("GET", "/voice/regions")
|
|
|
|
async def start_thread_from_message(
|
|
self,
|
|
channel_id: "Snowflake",
|
|
message_id: "Snowflake",
|
|
payload: Dict[str, Any],
|
|
) -> Dict[str, Any]:
|
|
"""Starts a new thread from an existing message."""
|
|
return await self.request(
|
|
"POST",
|
|
f"/channels/{channel_id}/messages/{message_id}/threads",
|
|
payload=payload,
|
|
)
|
|
|
|
async def start_thread_without_message(
|
|
self, channel_id: "Snowflake", payload: Dict[str, Any]
|
|
) -> Dict[str, Any]:
|
|
"""Starts a new thread that is not attached to a message."""
|
|
return await self.request(
|
|
"POST", f"/channels/{channel_id}/threads", payload=payload
|
|
)
|
|
|
|
async def join_thread(self, channel_id: "Snowflake") -> None:
|
|
"""Joins the current user to a thread."""
|
|
await self.request("PUT", f"/channels/{channel_id}/thread-members/@me")
|
|
|
|
async def leave_thread(self, channel_id: "Snowflake") -> None:
|
|
"""Removes the current user from a thread."""
|
|
await self.request("DELETE", f"/channels/{channel_id}/thread-members/@me")
|