feat: Implement OAuth2 endpoints (#3114)

* add OAuth2 routes

* Add oauth2 methods to rest

* Add rest manager methods, Add token params

* Add authorization headers

* Add auth to editUserApplicationRoleConnection

* fix logging header always displaying bot as auth

* Add OAuth2Scope enum

* Start testing ratelimit handling

* Fix now scopes are separated by a space

* move webhook object to DiscordAccessTokenResponse

* convert payload to snake_case

* fix some typings

* more types fixes

* add support for upserting commands with tokens

* handle correctly ratelimit and concurrently

* add guild to DiscordAccessTokenResponse

* Add oauth2 create link function

* Fix removeTokenPrefix to support Bearer tokens

* update jsdoc comment for removeTokenPrefix

* fix removeTokenPrefix unit tests

* fix see link on getMember and getCurrentMember

* add bot helpers and fix some types

* Use objects to pass the bearer tokens

* fix Deno issue with Buffer.from

* Merge 'upstream/main' into feat/oauth2 to fix merge conflict

* Fix debug queue logging

* keep only 1 route for current user

* add Bearer prefixed url to the rest of the logs

---------

Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com>
This commit is contained in:
Fleny
2023-09-19 18:23:22 +02:00
committed by GitHub
parent c92649029b
commit a7d645ec4b
12 changed files with 1140 additions and 85 deletions

View File

@@ -1,12 +1,18 @@
import type { CreateWebhook } from '@discordeno/rest'
import type {
AddDmRecipientOptions,
AddGuildMemberOptions,
ApplicationCommandPermissions,
AtLeastOne,
BeginGuildPrune,
BigString,
CamelizedDiscordAccessTokenResponse,
CamelizedDiscordApplicationRoleConnection,
CamelizedDiscordArchivedThreads,
CamelizedDiscordAuditLog,
CamelizedDiscordBan,
CamelizedDiscordConnection,
CamelizedDiscordCurrentAuthorization,
CamelizedDiscordFollowedChannel,
CamelizedDiscordGetGatewayBot,
CamelizedDiscordGuildPreview,
@@ -14,14 +20,19 @@ import type {
CamelizedDiscordInvite,
CamelizedDiscordInviteMetadata,
CamelizedDiscordModifyGuildWelcomeScreen,
CamelizedDiscordPartialGuild,
CamelizedDiscordPrunedCount,
CamelizedDiscordTokenExchange,
CamelizedDiscordTokenRevocation,
CamelizedDiscordVanityUrl,
CamelizedDiscordVoiceRegion,
CreateApplicationCommand,
CreateAutoModerationRuleOptions,
CreateChannelInvite,
CreateForumPostWithMessage,
CreateGlobalApplicationCommandOptions,
CreateGuild,
CreateGuildApplicationCommandOptions,
CreateGuildBan,
CreateGuildChannel,
CreateGuildEmoji,
@@ -44,7 +55,9 @@ import type {
EditScheduledEvent,
EditUserVoiceState,
ExecuteWebhook,
GetApplicationCommandPermissionOptions,
GetBans,
GetGroupDmOptions,
GetGuildAuditLog,
GetGuildPruneCountQuery,
GetInvite,
@@ -52,6 +65,7 @@ import type {
GetReactions,
GetScheduledEventUsers,
GetScheduledEvents,
GetUserGuilds,
GetWebhookMessageOptions,
InteractionCallbackData,
InteractionResponse,
@@ -69,6 +83,8 @@ import type {
SearchMembers,
StartThreadWithMessage,
StartThreadWithoutMessage,
UpsertGlobalApplicationCommandOptions,
UpsertGuildApplicationCommandOptions,
} from '@discordeno/types'
import { snakelize } from '@discordeno/utils'
import type { Bot } from './bot.js'
@@ -108,14 +124,14 @@ export function createBotHelpers(bot: Bot): BotHelpers {
createForumThread: async (channelId, options, reason) => {
return bot.transformers.channel(bot, { channel: snakelize(await bot.rest.createForumThread(channelId, options, reason)) })
},
createGlobalApplicationCommand: async (command) => {
return bot.transformers.applicationCommand(bot, snakelize(await bot.rest.createGlobalApplicationCommand(command)))
createGlobalApplicationCommand: async (command, options) => {
return bot.transformers.applicationCommand(bot, snakelize(await bot.rest.createGlobalApplicationCommand(command, options)))
},
createGuild: async (options) => {
return bot.transformers.guild(bot, { guild: snakelize(await bot.rest.createGuild(options)), shardId: 0 })
},
createGuildApplicationCommand: async (command, guildId) => {
return bot.transformers.applicationCommand(bot, snakelize(await bot.rest.createGuildApplicationCommand(command, guildId)))
createGuildApplicationCommand: async (command, guildId, options) => {
return bot.transformers.applicationCommand(bot, snakelize(await bot.rest.createGuildApplicationCommand(command, guildId, options)))
},
createGuildFromTemplate: async (templateCode, options) => {
return bot.transformers.guild(bot, { guild: snakelize(await bot.rest.createGuildFromTemplate(templateCode, options)), shardId: 0 })
@@ -235,11 +251,23 @@ export function createBotHelpers(bot: Bot): BotHelpers {
getApplicationInfo: async () => {
return bot.transformers.application(bot, snakelize(await bot.rest.getApplicationInfo()))
},
getApplicationCommandPermission: async (guildId, commandId) => {
return bot.transformers.applicationCommandPermission(bot, snakelize(await bot.rest.getApplicationCommandPermission(guildId, commandId)))
getCurrentAuthenticationInfo: async (bearerToken) => {
return await bot.rest.getCurrentAuthenticationInfo(bearerToken)
},
getApplicationCommandPermissions: async (guildId) => {
return (await bot.rest.getApplicationCommandPermissions(guildId)).map((res) =>
exchangeToken: async (options) => {
return await bot.rest.exchangeToken(options)
},
revokeToken: async (options) => {
return await bot.rest.revokeToken(options)
},
getApplicationCommandPermission: async (guildId, commandId, options) => {
const res = await bot.rest.getApplicationCommandPermission(guildId, commandId, options)
const snakedRes = snakelize(res)
return bot.transformers.applicationCommandPermission(bot, snakedRes)
},
getApplicationCommandPermissions: async (guildId, options) => {
return (await bot.rest.getApplicationCommandPermissions(guildId, options)).map((res) =>
bot.transformers.applicationCommandPermission(bot, snakelize(res)),
)
},
@@ -277,6 +305,9 @@ export function createBotHelpers(bot: Bot): BotHelpers {
getDmChannel: async (userId) => {
return bot.transformers.channel(bot, { channel: snakelize(await bot.rest.getDmChannel(userId)) })
},
getGroupDmChannel: async (options) => {
return bot.transformers.channel(bot, { channel: snakelize(await bot.rest.getGroupDmChannel(options)) })
},
getEmoji: async (guildId, emojiId) => {
return bot.transformers.emoji(bot, snakelize(await bot.rest.getEmoji(guildId, emojiId)))
},
@@ -298,6 +329,9 @@ export function createBotHelpers(bot: Bot): BotHelpers {
getGuild: async (guildId, options) => {
return bot.transformers.guild(bot, { guild: snakelize(await bot.rest.getGuild(guildId, options)), shardId: 0 })
},
getGuilds: async (bearerToken, options) => {
return await bot.rest.getGuilds(bearerToken, options)
},
getGuildApplicationCommand: async (commandId, guildId) => {
return bot.transformers.applicationCommand(bot, snakelize(await bot.rest.getGuildApplicationCommand(commandId, guildId)))
},
@@ -405,6 +439,15 @@ export function createBotHelpers(bot: Bot): BotHelpers {
getUser: async (id) => {
return bot.transformers.user(bot, snakelize(await bot.rest.getUser(id)))
},
getCurrentUser: async (bearerToken) => {
return bot.transformers.user(bot, snakelize(await bot.rest.getCurrentUser(bearerToken)))
},
getUserConnections: async (bearerToken) => {
return await bot.rest.getUserConnections(bearerToken)
},
getUserApplicationRoleConnection: async (bearerToken, applicationId) => {
return await bot.rest.getUserApplicationRoleConnection(bearerToken, applicationId)
},
getVanityUrl: async (guildId) => {
return await bot.rest.getVanityUrl(guildId)
},
@@ -449,11 +492,15 @@ export function createBotHelpers(bot: Bot): BotHelpers {
syncGuildTemplate: async (guildId) => {
return bot.transformers.template(bot, snakelize(await bot.rest.syncGuildTemplate(guildId)))
},
upsertGlobalApplicationCommands: async (commands) => {
return (await bot.rest.upsertGlobalApplicationCommands(commands)).map((res) => bot.transformers.applicationCommand(bot, snakelize(res)))
upsertGlobalApplicationCommands: async (commands, options) => {
return (await bot.rest.upsertGlobalApplicationCommands(commands, options)).map((res) =>
bot.transformers.applicationCommand(bot, snakelize(res)),
)
},
upsertGuildApplicationCommands: async (guildId, commands) => {
return (await bot.rest.upsertGuildApplicationCommands(guildId, commands)).map((res) => bot.transformers.applicationCommand(bot, snakelize(res)))
upsertGuildApplicationCommands: async (guildId, commands, options) => {
return (await bot.rest.upsertGuildApplicationCommands(guildId, commands, options)).map((res) =>
bot.transformers.applicationCommand(bot, snakelize(res)),
)
},
editBotMember: async (guildId, options, reason) => {
return bot.transformers.member(bot, snakelize(await bot.rest.editBotMember(guildId, options, reason)), guildId, bot.id)
@@ -464,6 +511,10 @@ export function createBotHelpers(bot: Bot): BotHelpers {
getMember: async (guildId, userId) => {
return bot.transformers.member(bot, snakelize(await bot.rest.getMember(guildId, userId)), guildId, userId)
},
getCurrentMember: async (guildId, bearerToken) => {
const res = await bot.rest.getCurrentMember(guildId, bearerToken)
return bot.transformers.member(bot, snakelize(res), guildId, bot.transformers.snowflake(res.user.id))
},
getMembers: async (guildId, options) => {
return (await bot.rest.getMembers(guildId, options)).map((res) =>
bot.transformers.member(bot, snakelize(res), guildId, bot.transformers.snowflake(res.user.id)),
@@ -490,6 +541,12 @@ export function createBotHelpers(bot: Bot): BotHelpers {
addThreadMember: async (channelId, userId) => {
return await bot.rest.addThreadMember(channelId, userId)
},
addDmRecipient: async (channelId, userId, options) => {
return await bot.rest.addDmRecipient(channelId, userId, options)
},
addGuildMember: async (guildId, userId, options) => {
return await bot.rest.addGuildMember(guildId, userId, options)
},
deleteAutomodRule: async (guildId, ruleId, reason) => {
return await bot.rest.deleteAutomodRule(guildId, ruleId, reason)
},
@@ -580,6 +637,9 @@ export function createBotHelpers(bot: Bot): BotHelpers {
editUserVoiceState: async (guildId, options) => {
return await bot.rest.editUserVoiceState(guildId, options)
},
editUserApplicationRoleConnection: async (bearerToken, applicationId, options) => {
return await bot.rest.editUserApplicationRoleConnection(bearerToken, applicationId, options)
},
joinThread: async (channelId) => {
return await bot.rest.joinThread(channelId)
},
@@ -595,6 +655,9 @@ export function createBotHelpers(bot: Bot): BotHelpers {
removeThreadMember: async (channelId, userId) => {
return await bot.rest.removeThreadMember(channelId, userId)
},
removeDmRecipient: async (channelId, userId) => {
return await bot.rest.removeDmRecipient(channelId, userId)
},
sendInteractionResponse: async (interactionId, token, options) => {
return await bot.rest.sendInteractionResponse(interactionId, token, options)
},
@@ -624,9 +687,13 @@ export interface BotHelpers {
createChannel: (guildId: BigString, options: CreateGuildChannel, reason?: string) => Promise<Channel>
createEmoji: (guildId: BigString, options: CreateGuildEmoji, reason?: string) => Promise<Emoji>
createForumThread: (channelId: BigString, options: CreateForumPostWithMessage, reason?: string) => Promise<Channel>
createGlobalApplicationCommand: (command: CreateApplicationCommand) => Promise<ApplicationCommand>
createGlobalApplicationCommand: (command: CreateApplicationCommand, options?: CreateGlobalApplicationCommandOptions) => Promise<ApplicationCommand>
createGuild: (options: CreateGuild) => Promise<Guild>
createGuildApplicationCommand: (command: CreateApplicationCommand, guildId: BigString) => Promise<ApplicationCommand>
createGuildApplicationCommand: (
command: CreateApplicationCommand,
guildId: BigString,
options?: CreateGuildApplicationCommandOptions,
) => Promise<ApplicationCommand>
createGuildFromTemplate: (templateCode: string, options: CreateGuildFromTemplate) => Promise<Guild>
createGuildSticker: (guildId: BigString, options: CreateGuildStickerOptions, reason?: string) => Promise<Sticker>
createGuildTemplate: (guildId: BigString, options: CreateTemplate) => Promise<Template>
@@ -673,12 +740,24 @@ export interface BotHelpers {
editWebhookWithToken: (webhookId: BigString, token: string, options: Omit<ModifyWebhook, 'channelId'>) => Promise<Webhook>
editWelcomeScreen: (guildId: BigString, options: CamelizedDiscordModifyGuildWelcomeScreen, reason?: string) => Promise<WelcomeScreen>
editWidgetSettings: (guildId: BigString, options: CamelizedDiscordGuildWidgetSettings, reason?: string) => Promise<GuildWidgetSettings>
editUserApplicationRoleConnection: (
bearerToken: string,
applicationId: BigString,
options: CamelizedDiscordApplicationRoleConnection,
) => Promise<CamelizedDiscordApplicationRoleConnection>
executeWebhook: (webhookId: BigString, token: string, options: ExecuteWebhook) => Promise<Message | undefined>
followAnnouncement: (sourceChannelId: BigString, targetChannelId: BigString) => Promise<CamelizedDiscordFollowedChannel>
getActiveThreads: (guildId: BigString) => Promise<{ threads: Channel[]; members: ThreadMember[] }>
getApplicationInfo: () => Promise<Application>
getApplicationCommandPermission: (guildId: BigString, commandId: BigString) => Promise<ApplicationCommandPermission>
getApplicationCommandPermissions: (guildId: BigString) => Promise<ApplicationCommandPermission[]>
getCurrentAuthenticationInfo: (bearerToken: string) => Promise<CamelizedDiscordCurrentAuthorization>
exchangeToken: (options: CamelizedDiscordTokenExchange) => Promise<CamelizedDiscordAccessTokenResponse>
revokeToken: (options: CamelizedDiscordTokenRevocation) => Promise<void>
getApplicationCommandPermission: (
guildId: BigString,
commandId: BigString,
options?: GetApplicationCommandPermissionOptions,
) => Promise<ApplicationCommandPermission>
getApplicationCommandPermissions: (guildId: BigString, options?: GetApplicationCommandPermissionOptions) => Promise<ApplicationCommandPermission[]>
getAuditLog: (guildId: BigString, options?: GetGuildAuditLog) => Promise<CamelizedDiscordAuditLog>
getAutomodRule: (guildId: BigString, ruleId: BigString) => Promise<AutoModerationRule>
getAutomodRules: (guildId: BigString) => Promise<AutoModerationRule[]>
@@ -690,6 +769,7 @@ export interface BotHelpers {
getChannels: (guildId: BigString) => Promise<Channel[]>
getChannelWebhooks: (channelId: BigString) => Promise<Webhook[]>
getDmChannel: (userId: BigString) => Promise<Channel>
getGroupDmChannel: (options: GetGroupDmOptions) => Promise<Channel>
getEmoji: (guildId: BigString, emojiId: BigString) => Promise<Emoji>
getEmojis: (guildId: BigString) => Promise<Emoji[]>
getFollowupMessage: (token: string, messageId: BigString) => Promise<Message>
@@ -697,6 +777,7 @@ export interface BotHelpers {
getGlobalApplicationCommand: (commandId: BigString) => Promise<ApplicationCommand>
getGlobalApplicationCommands: () => Promise<ApplicationCommand[]>
getGuild: (guildId: BigString, options?: { counts?: boolean }) => Promise<Guild>
getGuilds: (bearerToken: string, options?: GetUserGuilds) => Promise<CamelizedDiscordPartialGuild[]>
getGuildApplicationCommand: (commandId: BigString, guildId: BigString) => Promise<ApplicationCommand>
getGuildApplicationCommands: (guildId: BigString) => Promise<ApplicationCommand[]>
getGuildPreview: (guildId: BigString) => Promise<CamelizedDiscordGuildPreview>
@@ -732,6 +813,9 @@ export interface BotHelpers {
getThreadMembers: (channelId: BigString) => Promise<ThreadMember[]>
getReactions: (channelId: BigString, messageId: BigString, reaction: string, options?: GetReactions) => Promise<User[]>
getUser: (id: BigString) => Promise<User>
getCurrentUser: (bearerToken: string) => Promise<User>
getUserConnections: (bearerToken: string) => Promise<CamelizedDiscordConnection[]>
getUserApplicationRoleConnection: (bearerToken: string, applicationId: BigString) => Promise<CamelizedDiscordApplicationRoleConnection>
getVanityUrl: (guildId: BigString) => Promise<CamelizedDiscordVanityUrl>
getVoiceRegions: (guildId: BigString) => Promise<CamelizedDiscordVoiceRegion[]>
getWebhook: (webhookId: BigString) => Promise<Webhook>
@@ -746,11 +830,19 @@ export interface BotHelpers {
startThreadWithMessage: (channelId: BigString, messageId: BigString, options: StartThreadWithMessage, reason?: string) => Promise<Channel>
startThreadWithoutMessage: (channelId: BigString, options: StartThreadWithoutMessage, reason?: string) => Promise<Channel>
syncGuildTemplate: (guildId: BigString) => Promise<Template>
upsertGlobalApplicationCommands: (commands: CreateApplicationCommand[]) => Promise<ApplicationCommand[]>
upsertGuildApplicationCommands: (guildId: BigString, commands: CreateApplicationCommand[]) => Promise<ApplicationCommand[]>
upsertGlobalApplicationCommands: (
commands: CreateApplicationCommand[],
options?: UpsertGlobalApplicationCommandOptions,
) => Promise<ApplicationCommand[]>
upsertGuildApplicationCommands: (
guildId: BigString,
commands: CreateApplicationCommand[],
options?: UpsertGuildApplicationCommandOptions,
) => Promise<ApplicationCommand[]>
editBotMember: (guildId: BigString, options: EditBotMemberOptions, reason?: string) => Promise<Member>
editMember: (guildId: BigString, userId: BigString, options: ModifyGuildMember, reason?: string) => Promise<Member>
getMember: (guildId: BigString, userId: BigString) => Promise<Member>
getCurrentMember: (guildId: BigString, bearerToken: string) => Promise<Member>
getMembers: (guildId: BigString, options: ListGuildMembers) => Promise<Member[]>
pruneMembers: (guildId: BigString, options: BeginGuildPrune, reason?: string) => Promise<{ pruned: number | null }>
searchMembers: (guildId: BigString, query: string, options?: Omit<SearchMembers, 'query'>) => Promise<Member[]>
@@ -759,6 +851,8 @@ export interface BotHelpers {
addReactions: (channelId: BigString, messageId: BigString, reactions: string[], ordered?: boolean) => Promise<void>
addRole: (guildId: BigString, userId: BigString, roleId: BigString, reason?: string) => Promise<void>
addThreadMember: (channelId: BigString, userId: BigString) => Promise<void>
addDmRecipient: (channelId: BigString, userId: BigString, options: AddDmRecipientOptions) => Promise<void>
addGuildMember: (guildId: BigString, userId: BigString, options: AddGuildMemberOptions) => Promise<void>
deleteAutomodRule: (guildId: BigString, ruleId: BigString, reason?: string) => Promise<void>
deleteChannel: (channelId: BigString, reason?: string) => Promise<void>
deleteChannelPermissionOverride: (channelId: BigString, overwriteId: BigString, reason?: string) => Promise<void>
@@ -794,6 +888,7 @@ export interface BotHelpers {
leaveThread: (channelId: BigString) => Promise<void>
removeRole: (guildId: BigString, userId: BigString, roleId: BigString, reason?: string) => Promise<void>
removeThreadMember: (channelId: BigString, userId: BigString) => Promise<void>
removeDmRecipient: (channelId: BigString, userId: BigString) => Promise<void>
sendInteractionResponse: (interactionId: BigString, token: string, options: InteractionResponse) => Promise<void>
triggerTypingIndicator: (channelId: BigString) => Promise<void>
banMember: (guildId: BigString, userId: BigString, options?: CreateGuildBan, reason?: string) => Promise<void>

View File

@@ -1,5 +1,7 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable no-const-assign */
import { Buffer } from 'node:buffer'
import { calculateBits, camelToSnakeCase, camelize, delay, getBotIdFromToken, logger, processReactionString, urlToBase64 } from '@discordeno/utils'
import { createInvalidRequestBucket } from './invalidBucket.js'
@@ -9,12 +11,16 @@ import {
InteractionResponseTypes,
type BigString,
type Camelize,
type DiscordAccessTokenResponse,
type DiscordApplication,
type DiscordApplicationCommand,
type DiscordApplicationRoleConnection,
type DiscordAuditLog,
type DiscordAutoModerationRule,
type DiscordBan,
type DiscordChannel,
type DiscordConnection,
type DiscordCurrentAuthorization,
type DiscordEmoji,
type DiscordFollowedChannel,
type DiscordGetGatewayBot,
@@ -31,6 +37,7 @@ import {
type DiscordMember,
type DiscordMemberWithUser,
type DiscordMessage,
type DiscordPartialGuild,
type DiscordPrunedCount,
type DiscordRole,
type DiscordScheduledEvent,
@@ -48,7 +55,7 @@ import {
type ModifyGuildTemplate,
} from '@discordeno/types'
import { createRoutes } from './routes.js'
import type { CreateRequestBodyOptions, CreateRestManagerOptions, RestManager, SendRequestOptions } from './types.js'
import type { CreateRequestBodyOptions, CreateRestManagerOptions, MakeRequestOptions, RestManager, SendRequestOptions } from './types.js'
// TODO: make dynamic based on package.json file
const version = '19.0.0-alpha.1'
@@ -91,8 +98,10 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
routes: createRoutes(),
checkRateLimits(url) {
const ratelimited = rest.rateLimitedPaths.get(url)
checkRateLimits(url, headers) {
const authHeader = headers?.authorization ?? ''
const ratelimited = rest.rateLimitedPaths.get(`${authHeader}${url}`)
const global = rest.rateLimitedPaths.get('global')
const now = Date.now()
@@ -178,9 +187,19 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
// Have to use changeToDiscordFormat or else JSON.stringify may throw an error for the presence of BigInt(s) in the json
form.append('payload_json', JSON.stringify(rest.changeToDiscordFormat({ ...options.body, files: undefined })))
body = form
// No need to set the `content-type` header since `fetch` does that automatically for us when we use a `FormData` object.
body = form
} else if (options?.body && options.headers && options.headers['content-type'] === 'application/x-www-form-urlencoded') {
// OAuth2 body handling
const formBody: string[] = []
const discordBody = rest.changeToDiscordFormat(options.body)
for (const prop in discordBody) {
formBody.push(`${encodeURIComponent(prop)}=${encodeURIComponent(discordBody[prop])}`)
}
body = formBody.join('&')
} else if (options?.body !== undefined) {
if (options.body instanceof FormData) {
body = options.body
@@ -235,7 +254,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
},
/** Processes the rate limit headers and determines if it needs to be rate limited and returns the bucket id if available */
processHeaders(url, headers) {
processHeaders(url, headers, requestAuthorization) {
let rateLimited = false
// GET ALL NECESSARY HEADERS
@@ -247,7 +266,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
const bucketId = headers.get(RATE_LIMIT_BUCKET_HEADER) ?? undefined
const limit = headers.get(RATE_LIMIT_LIMIT_HEADER)
rest.queues.get(url)?.handleCompletedRequest({
rest.queues.get(`${requestAuthorization}${url}`)?.handleCompletedRequest({
remaining: remaining ? Number(remaining) : undefined,
interval: retryAfter ? Number(retryAfter) * 1000 : undefined,
max: limit ? Number(limit) : undefined,
@@ -258,7 +277,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
rateLimited = true
// SAVE THE URL AS LIMITED, IMPORTANT FOR NEW REQUESTS BY USER WITHOUT BUCKET
rest.rateLimitedPaths.set(url, {
rest.rateLimitedPaths.set(`${requestAuthorization}${url}`, {
url,
resetTimestamp: reset,
bucketId,
@@ -266,7 +285,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
// SAVE THE BUCKET AS LIMITED SINCE DIFFERENT URLS MAY SHARE A BUCKET
if (bucketId) {
rest.rateLimitedPaths.set(bucketId, {
rest.rateLimitedPaths.set(`${requestAuthorization}${bucketId}`, {
url,
resetTimestamp: reset,
bucketId,
@@ -295,7 +314,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
})
if (bucketId) {
rest.rateLimitedPaths.set(bucketId, {
rest.rateLimitedPaths.set(`${requestAuthorization}${bucketId}`, {
url: 'global',
resetTimestamp: globalReset,
bucketId,
@@ -313,7 +332,15 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
const url = `${rest.baseUrl}/v${rest.version}${options.route}`
const payload = rest.createRequestBody(options.method, options.requestBodyOptions)
logger.debug(`sending request to ${url}`, 'with payload:', { ...payload, headers: { ...payload.headers, authorization: 'Bot tokenhere' } })
const loggingHeaders = { ...payload.headers }
const authenticationScheme = payload.headers.authorization?.split(' ')[0]
if (payload.headers.authorization) {
loggingHeaders.authorization = `${authenticationScheme} tokenhere`
}
logger.debug(`sending request to ${url}`, 'with payload:', { ...payload, headers: loggingHeaders })
const response = await fetch(url, payload).catch(async (error) => {
logger.error(error)
// Mark request and completed
@@ -331,7 +358,11 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
rest.invalidBucket.handleCompletedRequest(response.status, response.headers.get(RATE_LIMIT_SCOPE_HEADER) === 'shared')
// Set the bucket id if it was available on the headers
const bucketId = rest.processHeaders(rest.simplifyUrl(options.route, options.method), response.headers)
const bucketId = rest.processHeaders(
rest.simplifyUrl(options.route, options.method),
response.headers,
authenticationScheme === 'Bearer' ? payload.headers.authorization : '',
)
if (bucketId) options.bucketId = bucketId
if (response.status < HttpResponseCode.Success || response.status >= HttpResponseCode.Error) {
@@ -406,18 +437,20 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
return
}
const queue = rest.queues.get(url)
const authHeader = request.requestBodyOptions?.headers?.authorization ?? ''
const queue = rest.queues.get(`${authHeader}${url}`)
if (queue !== undefined) {
queue.makeRequest(request)
} else {
// CREATES A NEW QUEUE
const bucketQueue = new Queue(rest, { url, deleteQueueDelay: rest.deleteQueueDelay })
const bucketQueue = new Queue(rest, { url, deleteQueueDelay: rest.deleteQueueDelay, authentication: authHeader })
// Add request to queue
bucketQueue.makeRequest(request)
// Save queue
rest.queues.set(url, bucketQueue)
rest.queues.set(`${authHeader}${url}`, bucketQueue)
}
},
@@ -511,6 +544,10 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
await rest.put(rest.routes.channels.threads.user(channelId, userId))
},
async addDmRecipient(channelId, userId, body) {
await rest.put(rest.routes.channels.dmRecipient(channelId, userId), { body })
},
async createAutomodRule(guildId, body, reason) {
return await rest.post<DiscordAutoModerationRule>(rest.routes.guilds.automod.rules(guildId), { body, reason })
},
@@ -523,16 +560,34 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
return await rest.post<DiscordEmoji>(rest.routes.guilds.emojis(guildId), { body, reason })
},
async createGlobalApplicationCommand(body) {
return await rest.post<DiscordApplicationCommand>(rest.routes.interactions.commands.commands(rest.applicationId), { body })
async createGlobalApplicationCommand(body, options) {
const restOptions: MakeRequestOptions = { body }
if (options?.bearerToken) {
restOptions.unauthorized = true
restOptions.headers = {
authorization: `Bearer ${options.bearerToken}`,
}
}
return await rest.post<DiscordApplicationCommand>(rest.routes.interactions.commands.commands(rest.applicationId), restOptions)
},
async createGuild(body) {
return await rest.post<DiscordGuild>(rest.routes.guilds.all(), { body })
},
async createGuildApplicationCommand(body, guildId) {
return await rest.post<DiscordApplicationCommand>(rest.routes.interactions.commands.guilds.all(rest.applicationId, guildId), { body })
async createGuildApplicationCommand(body, guildId, options) {
const restOptions: MakeRequestOptions = { body }
if (options?.bearerToken) {
restOptions.unauthorized = true
restOptions.headers = {
authorization: `Bearer ${options.bearerToken}`,
}
}
return await rest.post<DiscordApplicationCommand>(rest.routes.interactions.commands.guilds.all(rest.applicationId, guildId), restOptions)
},
async createGuildFromTemplate(templateCode, body) {
@@ -719,7 +774,7 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
async editBotProfile(options) {
const avatar = options?.botAvatarURL ? await urlToBase64(options?.botAvatarURL) : options?.botAvatarURL
return await rest.patch<DiscordUser>(rest.routes.userBot(), {
return await rest.patch<DiscordUser>(rest.routes.currentUser(), {
body: {
username: options.username?.trim(),
avatar,
@@ -869,18 +924,78 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
return await rest.get<DiscordListActiveThreads>(rest.routes.channels.threads.active(guildId))
},
async getApplicationCommandPermission(guildId, commandId) {
async getApplicationCommandPermission(guildId, commandId, options) {
const restOptions: Omit<MakeRequestOptions, 'body'> = {}
if (options?.accessToken) {
restOptions.unauthorized = true
restOptions.headers = {
authorization: `Bearer ${options.accessToken}`,
}
}
return await rest.get<DiscordGuildApplicationCommandPermissions>(
rest.routes.interactions.commands.permission(rest.applicationId, guildId, commandId),
rest.routes.interactions.commands.permission(options?.applicationId ?? rest.applicationId, guildId, commandId),
restOptions,
)
},
async getApplicationCommandPermissions(guildId) {
return await rest.get<DiscordGuildApplicationCommandPermissions[]>(rest.routes.interactions.commands.permissions(rest.applicationId, guildId))
async getApplicationCommandPermissions(guildId, options) {
const restOptions: Omit<MakeRequestOptions, 'body'> = {}
if (options?.accessToken) {
restOptions.unauthorized = true
restOptions.headers = {
authorization: `Bearer ${options.accessToken}`,
}
}
return await rest.get<DiscordGuildApplicationCommandPermissions[]>(
rest.routes.interactions.commands.permissions(options?.applicationId ?? rest.applicationId, guildId),
restOptions,
)
},
async getApplicationInfo() {
return await rest.get<DiscordApplication>(rest.routes.oauth2Application())
return await rest.get<DiscordApplication>(rest.routes.oauth2.application())
},
async getCurrentAuthenticationInfo(token) {
return await rest.get<DiscordCurrentAuthorization>(rest.routes.oauth2.currentAuthorization(), {
headers: {
authorization: `Bearer ${token}`,
},
unauthorized: true,
})
},
async exchangeToken(body) {
const restOptions: MakeRequestOptions = {
body,
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
unauthorized: true,
}
if (body.grantType === 'client_credentials') {
const basicCredentials = Buffer.from(`${body.clientId}:${body.clientSecret}`)
restOptions.headers!.authorization = `Basic ${basicCredentials.toString('base64')}`
restOptions.body.scope = body.scope.join(' ')
}
return await rest.post<DiscordAccessTokenResponse>(rest.routes.oauth2.tokenExchange(), restOptions)
},
async revokeToken(body) {
await rest.post(rest.routes.oauth2.tokenRevoke(), {
body,
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
unauthorized: true,
})
},
async getAuditLog(guildId, options) {
@@ -929,6 +1044,12 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
})
},
async getGroupDmChannel(body) {
return await rest.post<DiscordChannel>(rest.routes.channels.dm(), {
body,
})
},
async getEmoji(guildId, emojiId) {
return await rest.get<DiscordEmoji>(rest.routes.guilds.emoji(guildId, emojiId))
},
@@ -957,6 +1078,15 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
return await rest.get<DiscordGuild>(rest.routes.guilds.guild(guildId, options.counts))
},
async getGuilds(token, options) {
return await rest.get<DiscordPartialGuild[]>(rest.routes.guilds.userGuilds(options), {
headers: {
authorization: `Bearer ${token}`,
},
unauthorized: true,
})
},
async getGuildApplicationCommand(commandId, guildId) {
return await rest.get<DiscordApplicationCommand>(rest.routes.interactions.commands.guilds.one(rest.applicationId, guildId, commandId))
},
@@ -1081,6 +1211,33 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
return await rest.get<DiscordUser>(rest.routes.user(id))
},
async getCurrentUser(token) {
return await rest.get<DiscordUser>(rest.routes.currentUser(), {
headers: {
authorization: `Bearer ${token}`,
},
unauthorized: true,
})
},
async getUserConnections(token) {
return await rest.get<DiscordConnection[]>(rest.routes.oauth2.connections(), {
headers: {
authorization: `Bearer ${token}`,
},
unauthorized: true,
})
},
async getUserApplicationRoleConnection(token, applicationId) {
return await rest.get<DiscordApplicationRoleConnection>(rest.routes.oauth2.roleConnections(applicationId), {
headers: {
authorization: `Bearer ${token}`,
},
unauthorized: true,
})
},
async getVanityUrl(guildId) {
return await rest.get<DiscordVanityUrl>(rest.routes.guilds.vanity(guildId))
},
@@ -1137,6 +1294,10 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
await rest.delete(rest.routes.channels.threads.user(channelId, userId))
},
async removeDmRecipient(channelId, userId) {
await rest.delete(rest.routes.channels.dmRecipient(channelId, userId))
},
async sendFollowupMessage(token, options) {
return await rest.post(rest.routes.webhooks.webhook(rest.applicationId, token), {
body: options,
@@ -1186,6 +1347,15 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
return await rest.get<DiscordMemberWithUser>(rest.routes.guilds.members.member(guildId, userId))
},
async getCurrentMember(guildId, token) {
return await rest.get<DiscordMemberWithUser>(rest.routes.guilds.members.currentMember(guildId), {
headers: {
authorization: `Bearer ${token}`,
},
unauthorized: true,
})
},
async getMembers(guildId, options) {
return await rest.get<DiscordMemberWithUser[]>(rest.routes.guilds.members.members(guildId, options))
},
@@ -1220,12 +1390,46 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage
await rest.post(rest.routes.channels.typing(channelId))
},
async upsertGlobalApplicationCommands(body) {
return await rest.put<DiscordApplicationCommand[]>(rest.routes.interactions.commands.commands(rest.applicationId), { body })
async upsertGlobalApplicationCommands(body, options) {
const restOptions: MakeRequestOptions = { body }
if (options?.bearerToken) {
restOptions.unauthorized = true
restOptions.headers = {
authorization: `Bearer ${options.bearerToken}`,
}
}
return await rest.put<DiscordApplicationCommand[]>(rest.routes.interactions.commands.commands(rest.applicationId), restOptions)
},
async upsertGuildApplicationCommands(guildId, body) {
return await rest.put<DiscordApplicationCommand[]>(rest.routes.interactions.commands.guilds.all(rest.applicationId, guildId), { body })
async upsertGuildApplicationCommands(guildId, body, options) {
const restOptions: MakeRequestOptions = { body }
if (options?.bearerToken) {
restOptions.unauthorized = true
restOptions.headers = {
authorization: `Bearer ${options.bearerToken}`,
}
}
return await rest.put<DiscordApplicationCommand[]>(rest.routes.interactions.commands.guilds.all(rest.applicationId, guildId), restOptions)
},
async editUserApplicationRoleConnection(token, applicationId, body) {
return await rest.put<DiscordApplicationRoleConnection>(rest.routes.oauth2.roleConnections(applicationId), {
body,
headers: {
authorization: `Bearer ${token}`,
},
unauthorized: true,
})
},
async addGuildMember(guildId, userId, body) {
return await rest.put(rest.routes.guilds.members.member(guildId, userId), {
body,
})
},
preferSnakeCase(enabled: boolean) {

View File

@@ -28,6 +28,8 @@ export class Queue {
frozenAt: number = 0
/** The time in milliseconds to wait before deleting this queue if it is empty. Defaults to 60000(one minute). */
deleteQueueDelay: number = 60000
/** The authentication header used for the OAuth2 request. Defaults to an empty string for non-OAuth2 requests */
authentication: string = ''
constructor(rest: RestManager, options: QueueOptions) {
this.rest = rest
@@ -38,6 +40,7 @@ export class Queue {
if (options.remaining) this.remaining = options.remaining
if (options.timeoutId) this.timeoutId = options.timeoutId
if (options.deleteQueueDelay) this.deleteQueueDelay = options.deleteQueueDelay
if (options.authentication) this.authentication = options.authentication
}
/** Check if there is any remaining requests that are allowed. */
@@ -68,7 +71,7 @@ export class Queue {
this.processing = true
while (this.waiting.length > 0) {
logger.debug(`[Queue] ${this.url} process waiting while loop ran.`)
logger.debug(`[Queue] ${this.isOauth2Queue() ? '' : 'Bearer '}${this.url} process waiting while loop ran.`)
if (this.isRequestAllowed()) {
// Resolve the next item in the queue
this.waiting.shift()?.()
@@ -90,7 +93,7 @@ export class Queue {
this.processingPending = true
while (this.pending.length > 0) {
logger.debug(`Queue ${this.url} process pending while loop ran with ${this.pending.length}.`)
logger.debug(`Queue ${this.isOauth2Queue() ? '' : 'Bearer '}${this.url} process pending while loop ran with ${this.pending.length}.`)
if (!this.firstRequest && !this.isRequestAllowed()) {
const now = Date.now()
const future = this.frozenAt + this.interval
@@ -102,13 +105,12 @@ export class Queue {
if (request) {
const basicURL = this.rest.simplifyUrl(request.route, request.method)
// IF THIS URL IS STILL RATE LIMITED, TRY AGAIN
// If this url is still rate limited, try again
const urlResetIn = this.rest.checkRateLimits(basicURL)
const urlResetIn = this.rest.checkRateLimits(basicURL, request.requestBodyOptions?.headers)
if (urlResetIn) await delay(urlResetIn)
// IF A BUCKET EXISTS, CHECK THE BUCKET'S RATE LIMITS
const bucketResetIn = request.bucketId ? this.rest.checkRateLimits(request.bucketId) : false
const bucketResetIn = request.bucketId ? this.rest.checkRateLimits(request.bucketId, request.requestBodyOptions?.headers) : false
if (bucketResetIn) await delay(bucketResetIn)
this.firstRequest = false
@@ -133,7 +135,7 @@ export class Queue {
}
}
logger.debug(`Queue ${this.url} process pending while loop exited with ${this.pending.length}.`)
logger.debug(`Queue ${this.isOauth2Queue() ? '' : 'Bearer '}${this.url} process pending while loop exited with ${this.pending.length}.`)
// Mark as false so next pending request can be triggered by new loop.
this.processingPending = false
@@ -172,11 +174,11 @@ export class Queue {
return
}
logger.debug(`[Queue] ${this.url}. Delaying delete for ${this.deleteQueueDelay}ms`)
logger.debug(`[Queue] ${this.isOauth2Queue() ? '' : 'Bearer '}${this.url}. Delaying delete for ${this.deleteQueueDelay}ms`)
// Delete in a minute giving a bit of time to allow new requests that may reuse this queue
setTimeout(async () => {
if (!this.isQueueClearable()) {
logger.debug(`[Queue] ${this.url}. is not clearable. Restarting processing of queue.`)
logger.debug(`[Queue] ${this.isOauth2Queue() ? '' : 'Bearer '}${this.url}. is not clearable. Restarting processing of queue.`)
this.processPending()
return
}
@@ -184,8 +186,11 @@ export class Queue {
logger.debug(`[Queue] ${this.url}. Deleting`)
if (this.timeoutId) clearTimeout(this.timeoutId)
// No requests have been requested for this queue so we nuke this queue
this.rest.queues.delete(this.url)
logger.debug(`[Queue] ${this.url}. Deleted! Remaining: (${this.rest.queues.size})`, [...this.rest.queues.keys()])
this.rest.queues.delete(`${this.authentication}${this.url}`)
logger.debug(
`[Queue] ${this.url}. Deleted! Remaining: (${this.rest.queues.size})`,
[...this.rest.queues.values()].map((queue) => `${queue.isOauth2Queue() ? '' : 'Bearer '}${queue.url}`),
)
if (this.rest.queues.size) this.processPending()
}, this.deleteQueueDelay)
}
@@ -201,6 +206,10 @@ export class Queue {
return true
}
isOauth2Queue(): boolean {
return this.authentication === ''
}
}
export interface QueueOptions {
@@ -216,4 +225,6 @@ export interface QueueOptions {
url: string
/** The time in milliseconds to wait before deleting this queue if it is empty. Defaults to 60000(one minute). */
deleteQueueDelay?: number
/** Authentication used for the request. In non-OAuth2 situations should be an empty string. Defaults to an empty string */
authentication?: string
}

View File

@@ -46,6 +46,9 @@ export function createRoutes(): RestRoutes {
dm: () => {
return '/users/@me/channels'
},
dmRecipient: (channelId, userId) => {
return `/channels/${channelId}/recipients/${userId}`
},
pin: (channelId, messageId) => {
return `/channels/${channelId}/pins/${messageId}`
},
@@ -222,6 +225,22 @@ export function createRoutes(): RestRoutes {
all: () => {
return '/guilds'
},
userGuilds: (options) => {
let url = '/users/@me/guilds?'
if (options) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
if (options.after) url += `after=${options.after}`
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
if (options.before) url += `&before=${options.before}`
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
if (options.limit) url += `&limit=${options.limit}`
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
if (options.withCounts) url += `&with_counts=${options.withCounts}`
}
return url
},
auditlogs: (guildId, options) => {
let url = `/guilds/${guildId}/audit-logs?`
@@ -359,6 +378,9 @@ export function createRoutes(): RestRoutes {
member: (guildId, userId) => {
return `/guilds/${guildId}/members/${userId}`
},
currentMember: (guildId) => {
return `/users/@me/guilds/${guildId}/member`
},
members: (guildId, options) => {
let url = `/guilds/${guildId}/members?`
@@ -532,19 +554,37 @@ export function createRoutes(): RestRoutes {
},
},
// OAuth2 endpoints
oauth2: {
tokenExchange: () => {
return '/oauth2/token'
},
tokenRevoke: () => {
return '/oauth2/token/revoke'
},
currentAuthorization: () => {
return '/oauth2/@me'
},
application: () => {
return '/oauth2/applications/@me'
},
connections: () => {
return '/users/@me/connections'
},
roleConnections: (applicationId) => {
return `/users/@me/applications/${applicationId}/role-connection`
},
},
// User endpoints
user(userId) {
return `/users/${userId}`
},
userBot() {
currentUser() {
return '/users/@me'
},
oauth2Application() {
return '/oauth2/applications/@me'
},
gatewayBot() {
return '/gateway/bot'
},

View File

@@ -1,17 +1,23 @@
import type {
AddDmRecipientOptions,
AddGuildMemberOptions,
ApplicationCommandPermissions,
AtLeastOne,
BeginGuildPrune,
BigString,
Camelize,
CamelizedDiscordAccessTokenResponse,
CamelizedDiscordActiveThreads,
CamelizedDiscordApplication,
CamelizedDiscordApplicationCommand,
CamelizedDiscordApplicationRoleConnection,
CamelizedDiscordArchivedThreads,
CamelizedDiscordAuditLog,
CamelizedDiscordAutoModerationRule,
CamelizedDiscordBan,
CamelizedDiscordChannel,
CamelizedDiscordConnection,
CamelizedDiscordCurrentAuthorization,
CamelizedDiscordEmoji,
CamelizedDiscordFollowedChannel,
CamelizedDiscordGetGatewayBot,
@@ -27,6 +33,7 @@ import type {
CamelizedDiscordMemberWithUser,
CamelizedDiscordMessage,
CamelizedDiscordModifyGuildWelcomeScreen,
CamelizedDiscordPartialGuild,
CamelizedDiscordPrunedCount,
CamelizedDiscordRole,
CamelizedDiscordScheduledEvent,
@@ -35,6 +42,8 @@ import type {
CamelizedDiscordStickerPack,
CamelizedDiscordTemplate,
CamelizedDiscordThreadMember,
CamelizedDiscordTokenExchange,
CamelizedDiscordTokenRevocation,
CamelizedDiscordUser,
CamelizedDiscordVanityUrl,
CamelizedDiscordVoiceRegion,
@@ -44,7 +53,9 @@ import type {
CreateAutoModerationRuleOptions,
CreateChannelInvite,
CreateForumPostWithMessage,
CreateGlobalApplicationCommandOptions,
CreateGuild,
CreateGuildApplicationCommandOptions,
CreateGuildBan,
CreateGuildChannel,
CreateGuildEmoji,
@@ -67,14 +78,17 @@ import type {
EditUserVoiceState,
ExecuteWebhook,
FileContent,
GetApplicationCommandPermissionOptions,
GetBans,
GetGroupDmOptions,
GetGuildAuditLog,
GetGuildPruneCountQuery,
GetInvite,
GetMessagesOptions,
GetReactions,
GetScheduledEvents,
GetScheduledEventUsers,
GetScheduledEvents,
GetUserGuilds,
GetWebhookMessageOptions,
InteractionCallbackData,
InteractionResponse,
@@ -92,6 +106,8 @@ import type {
SearchMembers,
StartThreadWithMessage,
StartThreadWithoutMessage,
UpsertGlobalApplicationCommandOptions,
UpsertGuildApplicationCommandOptions,
} from '@discordeno/types'
import type { InvalidRequestBucket } from './invalidBucket.js'
import type { Queue } from './queue.js'
@@ -161,17 +177,22 @@ export interface RestManager {
/** The routes that are available for this manager. */
routes: RestRoutes
/** Whether or not the rest manager should keep objects in raw snake case from discord. */
preferSnakeCase: (enabled: boolean) => RestManager;
preferSnakeCase: (enabled: boolean) => RestManager
/** Check the rate limits for a url or a bucket. */
checkRateLimits: (url: string) => number | false
checkRateLimits: (url: string, headers?: Record<string, string>) => number | false
/** Reshapes and modifies the obj as needed to make it ready for discords api. */
changeToDiscordFormat: (obj: any) => any
/** Creates the request body and headers that are necessary to send a request. Will handle different types of methods and everything necessary for discord. */
createRequestBody: (method: RequestMethods, options?: CreateRequestBodyOptions) => RequestBody
/** This will create a infinite loop running in 1 seconds using tail recursion to keep rate limits clean. When a rate limit resets, this will remove it so the queue can proceed. */
processRateLimitedPaths: () => void
/** Processes the rate limit headers and determines if it needs to be rate limited and returns the bucket id if available */
processHeaders: (url: string, headers: Headers) => string | undefined
/**
* Processes the rate limit headers and determines if it needs to be rate limited and returns the bucket id if available
*
* @remarks
* The authenticationHeader should be defined ONLY if the request was done using a OAuth2 Access Token, in other cases it should be passed as an empty string
*/
processHeaders: (url: string, headers: Headers, authenticationHeader?: string) => string | undefined
/** Sends a request to the api. */
sendRequest: (options: SendRequestOptions) => Promise<void>
/** Split a url to separate rate limit buckets based on major/minor parameters. */
@@ -259,6 +280,35 @@ export interface RestManager {
* @see {@link https://discord.com/developers/docs/resources/channel#add-thread-member}
*/
addThreadMember: (channelId: BigString, userId: BigString) => Promise<void>
/**
* Adds a recipient to a group DM.
*
* @param channelId - The ID of the group dm to add the user to.
* @param userId - The user ID of the user to add to the group dm.
* @param options - The options for adding the user
*
* @remarks
* Requires an OAuth2 access token with the `gdm.join` scope
*
* @see {@link https://discord.com/developers/docs/resources/channel#group-dm-add-recipient}
*/
addDmRecipient: (channelId: BigString, userId: BigString, options: AddDmRecipientOptions) => Promise<void>
/**
* Adds a member to a guild.
*
* @param guildId - The ID of the thread to add the member to.
* @param userId - The user ID of the member to add to the thread.
* @param options - The options for the add of a guild member
*
* @remarks
* Requires the bot to be in the specified server
* Requires an OAuth2 access token with the `guilds.join` scope
*
* Fires a _Guild Member Add_ gateway event.
*
* @see {@link https://discord.com/developers/docs/resources/guild#add-guild-member}
*/
addGuildMember: (guildId: BigString, userId: BigString, options: AddGuildMemberOptions) => Promise<void>
/**
* Creates an automod rule in a guild.
*
@@ -336,6 +386,7 @@ export interface RestManager {
* Creates an application command accessible globally; across different guilds and channels.
*
* @param command - The command to create.
* @param options - Additional options for the endpoint
* @returns An instance of the created {@link ApplicationCommand}.
*
* @remarks
@@ -343,9 +394,15 @@ export interface RestManager {
* ⚠️ Global commands once created are cached for periods of __an hour__, so changes made to existing commands will take an hour to surface.
* ⚠️ You can only create up to 200 _new_ commands daily.
*
* When using the bearer token the token needs the `applications.commands.update` scope and must be a `Client grant` token.
* You will be able to update only your own application commands
*
* @see {@link https://discord.com/developers/docs/interactions/application-commands#create-global-application-command}
*/
createGlobalApplicationCommand: (command: CreateApplicationCommand) => Promise<CamelizedDiscordApplicationCommand>
createGlobalApplicationCommand: (
command: CreateApplicationCommand,
options?: CreateGlobalApplicationCommandOptions,
) => Promise<CamelizedDiscordApplicationCommand>
/**
* Creates a guild.
*
@@ -365,15 +422,23 @@ export interface RestManager {
*
* @param command - The command to create.
* @param guildId - The ID of the guild to create the command for.
* @param options - Additional options for the endpoint
* @returns An instance of the created {@link ApplicationCommand}.
*
* @remarks
* ⚠️ Creating a command with the same name as an existing command for your application will overwrite the old command.
* ⚠️ You can only create up to 200 _new_ commands daily.
*
* When using the bearer token the token needs the `applications.commands.update` scope and must be a `Client grant` token.
* You will be able to update only your own application commands
*
* @see {@link https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command}
*/
createGuildApplicationCommand: (command: CreateApplicationCommand, guildId: BigString) => Promise<CamelizedDiscordApplicationCommand>
createGuildApplicationCommand: (
command: CreateApplicationCommand,
guildId: BigString,
options?: CreateGuildApplicationCommandOptions,
) => Promise<CamelizedDiscordApplicationCommand>
/**
* Creates a guild from a template.
*
@@ -1264,6 +1329,24 @@ export interface RestManager {
* @see {@link https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state}
*/
editUserVoiceState: (guildId: BigString, options: EditUserVoiceState) => Promise<void>
/**
* Edit the current user application role connection for the application.
*
* @param bearerToken - The access token of the user
* @param applicationId - The id of the application to edit the role connection
* @param options - The options to edit
* @returns {CamelizedDiscordApplicationRoleConnection}
*
* @remarks
* This requires the `role_connections.write` scope.
*
* @see {@link https://discord.com/developers/docs/resources/user#update-user-application-role-connection}
*/
editUserApplicationRoleConnection: (
bearerToken: string,
applicationId: BigString,
options: CamelizedDiscordApplicationRoleConnection,
) => Promise<CamelizedDiscordApplicationRoleConnection>
/**
* Edits a webhook.
*
@@ -1398,25 +1481,63 @@ export interface RestManager {
getActiveThreads: (guildId: BigString) => Promise<CamelizedDiscordActiveThreads>
/** Get the applications info */
getApplicationInfo: () => Promise<CamelizedDiscordApplication>
/**
* Get the current authentication info for the authenticated user
*
* @param bearerToken - Any OAuth2 derived access token
* @returns An instance of {@link CamelizedDiscordCurrentAuthorization}
*
* @remarks
* The user object is not defined if the scopes do not include `identify`.
* In the user object, if defined, the email is not included if the scopes do not include `email`
*/
getCurrentAuthenticationInfo: (bearerToken: string) => Promise<CamelizedDiscordCurrentAuthorization>
/**
* Exchange the information to get a OAuth2 accessToken token
*
* @param options - The options to make the exchange with discord
*/
exchangeToken: (options: CamelizedDiscordTokenExchange) => Promise<CamelizedDiscordAccessTokenResponse>
/**
* Revoke an access_token
*
* @param options - The options to revoke the access_token
*/
revokeToken: (options: CamelizedDiscordTokenRevocation) => Promise<void>
/**
* Gets the permissions of a guild application command.
*
* @param guildId - The ID of the guild the command is registered in.
* @param commandId - The ID of the command to get the permissions of.
* @param options - The OAuth2 related optional parameters for the endpoint
* @returns An instance of {@link ApplicationCommandPermission}.
*
* @remarks
* Then specifying the options object the access token passed-in requires the OAuth2 scope `applications.commands.permissions.update`
*
* @see {@link https://discord.com/developers/docs/interactions/application-commands#get-application-command-permissions}
*/
getApplicationCommandPermission: (guildId: BigString, commandId: BigString) => Promise<CamelizedDiscordGuildApplicationCommandPermissions>
getApplicationCommandPermission: (
guildId: BigString,
commandId: BigString,
options?: GetApplicationCommandPermissionOptions,
) => Promise<CamelizedDiscordGuildApplicationCommandPermissions>
/**
* Gets the permissions of all application commands registered in a guild by the ID of the guild.
* Gets the permissions of all application commands registered in a guild by the ID of the guild and optionally an external application.
*
* @param guildId - The ID of the guild to get the permissions objects of.
* @param options - The OAuth2 related optional parameters for the endpoint
* @returns A collection of {@link ApplicationCommandPermission} objects assorted by command ID.
*
* @remarks
* Then specifying the options object the access token passed-in requires the OAuth2 scope `applications.commands.permissions.update`
*
* @see {@link https://discord.com/developers/docs/interactions/application-commands#get-guild-application-command-permissions}
*/
getApplicationCommandPermissions: (guildId: BigString) => Promise<CamelizedDiscordGuildApplicationCommandPermissions[]>
getApplicationCommandPermissions: (
guildId: BigString,
options?: GetApplicationCommandPermissionOptions,
) => Promise<CamelizedDiscordGuildApplicationCommandPermissions[]>
/**
* Gets a guild's audit log.
*
@@ -1548,6 +1669,22 @@ export interface RestManager {
* @see {@link https://discord.com/developers/docs/resources/user#create-dm}
*/
getDmChannel: (userId: BigString) => Promise<CamelizedDiscordChannel>
/**
* Create a new group DM channel with multiple users.
*
* @param options - The options for create a new group dm
* @returns An instance of {@link CamelizedDiscordChannel}.
*
* @remarks
* The access tokens require to have the `gdm.join` scope
*
* This endpoint is limited to 10 active group DMs.
*
* Fires a _Channel create_ gateway event.
*
* @see {@link https://discord.com/developers/docs/resources/user#create-group-dm}
*/
getGroupDmChannel: (options: GetGroupDmOptions) => Promise<CamelizedDiscordChannel>
/**
* Gets an emoji by its ID.
*
@@ -1613,6 +1750,19 @@ export interface RestManager {
* @see {@link https://discord.com/developers/docs/resources/guild#get-guild}
*/
getGuild: (guildId: BigString, options?: { counts?: boolean }) => Promise<CamelizedDiscordGuild>
/**
* Get the user guilds.
*
* @param bearerToken - The access token of the user
* @param options - The parameters for the fetching of the guild.
* @returns An instance of {@link Guild}.
*
* @remarks
* The access tokens needs to have the `guilds` scope
*
* @see {@link https://discord.com/developers/docs/resources/user#get-current-user-guilds}
*/
getGuilds: (bearerToken: string, options?: GetUserGuilds) => Promise<CamelizedDiscordPartialGuild[]>
/**
* Gets a guild application command by its ID.
*
@@ -1988,6 +2138,41 @@ export interface RestManager {
* @returns {CamelizedDiscordUser}
*/
getUser: (id: BigString) => Promise<CamelizedDiscordUser>
/**
* Get the current user data.
*
* @param bearerToken - The access token of the user
* @returns {CamelizedDiscordUser}
*
* @remarks
* This requires the `identify` scope.
*
* To get the mail this also requires the `email` scope
*/
getCurrentUser: (bearerToken: string) => Promise<CamelizedDiscordUser>
/**
* Get the current user connections.
*
* @param bearerToken - The access token of the user
* @returns {CamelizedDiscordConnection[]}
*
* @remarks
* This requires the `connections` scope.
*/
getUserConnections: (bearerToken: string) => Promise<CamelizedDiscordConnection[]>
/**
* Get the current user application role connection for the application.
*
* @param bearerToken - The access token of the user
* @param applicationId - The id of the application to get the role connection
* @returns {CamelizedDiscordApplicationRoleConnection}
*
* @remarks
* The access token requires the `role_connections.write` scope.
*
* @see {@link https://discord.com/developers/docs/resources/user#get-user-application-role-connection}
*/
getUserApplicationRoleConnection: (bearerToken: string, applicationId: BigString) => Promise<CamelizedDiscordApplicationRoleConnection>
/**
* Gets information about the vanity url of a guild.
*
@@ -2172,6 +2357,15 @@ export interface RestManager {
* @see {@link https://discord.com/developers/docs/resources/channel#remove-thread-member}
*/
removeThreadMember: (channelId: BigString, userId: BigString) => Promise<void>
/**
* Removes a member from a Group DM.
*
* @param channelId - The ID of the channel to remove the recipient user of.
* @param userId - The user ID of the user to remove.
*
* @see {@link https://discord.com/developers/docs/resources/channel#group-dm-remove-recipient}
*/
removeDmRecipient: (channelId: BigString, userId: BigString) => Promise<void>
/**
* Sends a message to a channel.
*
@@ -2315,6 +2509,7 @@ export interface RestManager {
* Re-registers the list of global application commands, overwriting the previous commands completely.
*
* @param commands - The list of commands to use to overwrite the previous list.
* @param options - Additional options for the endpoint.
* @returns A collection of {@link ApplicationCommand} objects assorted by command ID.
*
* @remarks
@@ -2322,14 +2517,21 @@ export interface RestManager {
*
* ⚠️ Commands that do not already exist will count towards the daily limit of _200_ new commands.
*
* When using the bearer token the token needs the `applications.commands.update` scope and must be a `Client grant` token.
* You will be able to update only your own application commands
*
* @see {@link https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands}
*/
upsertGlobalApplicationCommands: (commands: CreateApplicationCommand[]) => Promise<CamelizedDiscordApplicationCommand[]>
upsertGlobalApplicationCommands: (
commands: CreateApplicationCommand[],
options?: UpsertGlobalApplicationCommandOptions,
) => Promise<CamelizedDiscordApplicationCommand[]>
/**
* Re-registers the list of application commands registered in a guild, overwriting the previous commands completely.
*
* @param guildId - The ID of the guild whose list of commands to overwrite.
* @param commands - The list of commands to use to overwrite the previous list.
* @param options - Additional options for the endpoint.
* @returns A collection of {@link ApplicationCommand} objects assorted by command ID.
*
* @remarks
@@ -2337,9 +2539,16 @@ export interface RestManager {
*
* ⚠️ Commands that do not already exist will count towards the daily limit of _200_ new commands.
*
* When using the bearer token the token needs the `applications.commands.update` scope and must be a `Client grant` token.
* You will be able to update only your own application commands
*
* @see {@link https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-guild-application-commands}
*/
upsertGuildApplicationCommands: (guildId: BigString, commands: CreateApplicationCommand[]) => Promise<CamelizedDiscordApplicationCommand[]>
upsertGuildApplicationCommands: (
guildId: BigString,
commands: CreateApplicationCommand[],
options?: UpsertGuildApplicationCommandOptions,
) => Promise<CamelizedDiscordApplicationCommand[]>
/**
* Bans a user from a guild.
*
@@ -2398,6 +2607,19 @@ export interface RestManager {
* @see {@link https://discord.com/developers/docs/resources/guild#get-guild-member}
*/
getMember: (guildId: BigString, userId: BigString) => Promise<CamelizedDiscordMemberWithUser>
/**
* Gets the current member object.
*
* @param bearerToken - The access token of the user
* @param guildId - The ID of the guild to get the member object for.
* @returns An instance of {@link CamelizedDiscordMemberWithUser}.
*
* @remarks
* The access tokens needs the `guilds.members.read` scope
*
* @see {@link https://discord.com/developers/docs/resources/user#get-current-user-guild-member}
*/
getCurrentMember: (guildId: BigString, bearerToken: string) => Promise<CamelizedDiscordMemberWithUser>
/**
* Gets the list of members for a guild.
*

View File

@@ -7,6 +7,7 @@ import type {
GetMessagesOptions,
GetReactions,
GetScheduledEventUsers,
GetUserGuilds,
ListArchivedThreads,
ListGuildMembers,
} from '@discordeno/types'
@@ -14,10 +15,6 @@ import type {
export interface RestRoutes {
/** A specific user route. */
user: (id: BigString) => string
/** Current bot user route. */
userBot: () => string
// OAuth2
oauth2Application: () => string
// Gateway Bot
gatewayBot: () => string
// Nitro Sticker Packs
@@ -39,6 +36,8 @@ export interface RestRoutes {
bulk: (channelId: BigString) => string
/** Route for non-specific dm channel. */
dm: () => string
/** Route to add a user to an exiting group DM, requires an access token with the OAuth2 `gdm.join` scope */
dmRecipient: (channelId: BigString, userId: BigString) => string
/** Route for handling a specific pin. */
pin: (channelId: BigString, messageId: BigString) => string
/** Route for handling a channels pins. */
@@ -108,6 +107,8 @@ export interface RestRoutes {
guilds: {
/** Routes for handling a non-specific guild. */
all: () => string
/** Route for fetching an user guilds. Requires `guilds` OAuth2 scope */
userGuilds: (options?: GetUserGuilds) => string
/** Route for handling audit logs in a guild. */
auditlogs: (guildId: BigString, options?: GetGuildAuditLog) => string
/** Routes for a guilds automoderation. */
@@ -168,6 +169,8 @@ export interface RestRoutes {
bot: (guildId: BigString) => string
/** Route for handling a specific guild member. */
member: (guildId: BigString, userId: BigString) => string
/** Route to get the authenticated user. Requires the `guilds.members.read` OAuth2 scope */
currentMember: (guildId: BigString) => string
/** Route for handling non-specific guild members. */
members: (guildId: BigString, options?: ListGuildMembers) => string
/** Route for handling member searching in a guild. */
@@ -234,6 +237,23 @@ export interface RestRoutes {
message: (applicationId: BigString, token: string, messageId: BigString) => string
}
}
/** Routes related to OAuth2 */
oauth2: {
/** Route to generate a new access token */
tokenExchange: () => string
/** Route to revoke an OAuth2 access_token */
tokenRevoke: () => string
/** Route to get information about the current authorization. Requires an access token */
currentAuthorization: () => string
/** Route to get information about the current application. Requires an access token */
application: () => string
/** Route to get the connection the user has. Requires the `connections` OAuth2 scope */
connections: () => string
/** Route to handling role-connection for an application */
roleConnections: (applicationId: BigString) => string
}
/** Get information about the current OAuth2 user / bot user. If used with a OAuth2 token requires the `identify` OAuth2 scope */
currentUser: () => string
/** Route for handling a sticker. */
sticker: (stickerId: BigString) => string
/** Route for handling all voice regions. */

View File

@@ -1,4 +1,5 @@
import type {
DiscordAccessTokenResponse,
DiscordActionRow,
DiscordActiveThreads,
DiscordActivity,
@@ -14,6 +15,7 @@ import type {
DiscordApplicationCommandOption,
DiscordApplicationCommandOptionChoice,
DiscordApplicationCommandPermissions,
DiscordApplicationRoleConnection,
DiscordApplicationWebhook,
DiscordArchivedThreads,
DiscordAttachment,
@@ -31,12 +33,14 @@ import type {
DiscordChannelMention,
DiscordChannelPinsUpdate,
DiscordClientStatus,
DiscordConnection,
DiscordCreateApplicationCommand,
DiscordCreateForumPostWithMessage,
DiscordCreateGuildChannel,
DiscordCreateGuildEmoji,
DiscordCreateMessage,
DiscordCreateWebhook,
DiscordCurrentAuthorization,
DiscordDefaultReactionEmoji,
DiscordEditChannelPermissionOverridesOptions,
DiscordEmbed,
@@ -108,6 +112,7 @@ import type {
DiscordModifyGuildWelcomeScreen,
DiscordOptionalAuditEntryInfo,
DiscordOverwrite,
DiscordPartialGuild,
DiscordPresenceUpdate,
DiscordPrunedCount,
DiscordReaction,
@@ -133,6 +138,11 @@ import type {
DiscordThreadMemberUpdate,
DiscordThreadMembersUpdate,
DiscordThreadMetadata,
DiscordTokenExchange,
DiscordTokenExchangeAuthorizationCode,
DiscordTokenExchangeClientCredentials,
DiscordTokenExchangeRefreshToken,
DiscordTokenRevocation,
DiscordTypingStart,
DiscordUnavailableGuild,
DiscordUser,
@@ -157,6 +167,15 @@ export interface CamelizedDiscordGuildIntegrationsUpdate extends Camelize<Discor
export interface CamelizedDiscordTypingStart extends Camelize<DiscordTypingStart> {}
export interface CamelizedDiscordMember extends Camelize<DiscordMember> {}
export interface CamelizedDiscordApplication extends Camelize<DiscordApplication> {}
export interface CamelizedDiscordApplicationRoleConnection extends Camelize<DiscordApplicationRoleConnection> {}
export type CamelizedDiscordTokenExchange = Camelize<DiscordTokenExchange>
export interface CamelizedDiscordTokenExchangeAuthorizationCode extends Camelize<DiscordTokenExchangeAuthorizationCode> {}
export interface CamelizedDiscordTokenExchangeRefreshToken extends Camelize<DiscordTokenExchangeRefreshToken> {}
export interface CamelizedDiscordTokenExchangeClientCredentials extends Camelize<DiscordTokenExchangeClientCredentials> {}
export interface CamelizedDiscordAccessTokenResponse extends Camelize<DiscordAccessTokenResponse> {}
export interface CamelizedDiscordTokenRevocation extends Camelize<DiscordTokenRevocation> {}
export interface CamelizedDiscordCurrentAuthorization extends Camelize<DiscordCurrentAuthorization> {}
export interface CamelizedDiscordConnection extends Camelize<DiscordConnection> {}
export interface CamelizedDiscordTeam extends Camelize<DiscordTeam> {}
export interface CamelizedDiscordTeamMember extends Camelize<DiscordTeamMember> {}
export interface CamelizedDiscordWebhookUpdate extends Camelize<DiscordWebhookUpdate> {}
@@ -174,6 +193,7 @@ export type CamelizedDiscordWebhook = Camelize<DiscordWebhook>
export interface CamelizedDiscordIncomingWebhook extends Camelize<DiscordIncomingWebhook> {}
export interface CamelizedDiscordApplicationWebhook extends Camelize<DiscordApplicationWebhook> {}
export interface CamelizedDiscordGuild extends Camelize<DiscordGuild> {}
export interface CamelizedDiscordPartialGuild extends Camelize<DiscordPartialGuild> {}
export interface CamelizedDiscordRole extends Camelize<DiscordRole> {}
export interface CamelizedDiscordRoleTags extends Camelize<DiscordRoleTags> {}
export interface CamelizedDiscordEmoji extends Camelize<DiscordEmoji> {}

View File

@@ -6,6 +6,7 @@ import type {
ApplicationCommandTypes,
ApplicationFlags,
AuditLogEvents,
BigString,
ButtonStyles,
ChannelFlags,
ChannelTypes,
@@ -78,6 +79,135 @@ export interface DiscordUser {
banner?: string
}
/** https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes */
export enum OAuth2Scope {
/**
* Allows your app to fetch data from a user's "Now Playing/Recently Played" list
*
* @remarks
* This scope is not currently available for apps
*/
ActivitiesRead = 'activities.read',
/**
* Allows your app to update a user's activity
*
* @remarks
* This scope requires Discord approval to be used
*/
ActivitiesWrite = 'activities.write',
/** Allows your app to read build data for a user's applications */
ApplicationsBuildsRead = 'applications.builds.read',
/**
* Allows your app to upload/update builds for a user's applications
*
* @remarks
* This scope requires Discord approval to be used
*/
ApplicationsBuildsUpload = 'applications.builds.upload',
/** Allows your app to use Application Commands in a guild */
ApplicationsCommands = 'applications.commands',
/**
* Allows your app to update its Application Commands via this bearer token
*
* @remarks
* This scope can only be used when using a [Client Credential Grant](https://discord.com/developers/docs/topics/oauth2#client-credentials-grant)
*/
ApplicationsCommandsUpdate = 'applications.commands.update',
/** Allows your app to update permissions for its commands in a guild a user has permissions to */
ApplicationCommandsPermissionsUpdate = 'applications.commands.permissions.update',
/** Allows your app to read entitlements for a user's applications */
ApplicationsEntitlements = 'applications.entitlements',
/** Allows your app to read and update store data (SKUs, store listings, achievements, etc.) for a user's applications */
ApplicationsStoreUpdate = 'applications.store.update',
/** For oauth2 bots, this puts the bot in the user's selected guild by default */
Bot = 'bot',
/** Allows requests to [/users/@me/connections](https://discord.com/developers/docs/resources/user#get-user-connections) */
Connections = 'connections',
/**
* Allows your app to see information about the user's DMs and group DMs
*
* @remarks
* This scope requires Discord approval to be used
*/
DMChannelsRead = 'dm_channels.read',
/** Adds the `email` filed to [/users/@me](https://discord.com/developers/docs/resources/user#get-current-user) */
Email = 'email',
/** Allows your app to join users to a group dm */
GroupDMJoins = 'gdm.join',
/** Allows requests to [/users/@me/guilds](https://discord.com/developers/docs/resources/user#get-current-user-guilds) */
Guilds = 'guilds',
/** Allows requests to [/guilds/{guild.id}/members/{user.id}](https://discord.com/developers/docs/resources/guild#add-guild-member) */
GuildsJoin = 'guilds.join',
/** Allows requests to [/users/@me/guilds/{guild.id}/member](https://discord.com/developers/docs/resources/user#get-current-user-guild-member) */
GuildsMembersRead = 'guilds.members.read',
/**
* Allows requests to [/users/@me](https://discord.com/developers/docs/resources/user#get-current-user)
*
* @remarks
* The return object from [/users/@me](https://discord.com/developers/docs/resources/user#get-current-user)
* does NOT contain the `email` field unless the scope `email` is also used
*/
Identify = 'identify',
/**
* For local rpc server api access, this allows you to read messages from all client channels
* (otherwise restricted to channels/guilds your app creates)
*/
MessagesRead = 'messages.read',
/**
* Allows your app to know a user's friends and implicit relationships
*
* @remarks
* This scope requires Discord approval to be used
*/
RelationshipsRead = 'relationships.read',
/** Allows your app to update a user's connection and metadata for the app */
RoleConnectionsWrite = 'role_connections.write',
/**
* For local rpc server access, this allows you to control a user's local Discord client
*
* @remarks
* This scope requires Discord approval to be used
*/
RPC = 'rpc',
/**
* For local rpc server access, this allows you to update a user's activity
*
* @remarks
* This scope requires Discord approval to be used
*/
RPCActivitiesWrite = 'rpc.activities.write',
/**
* For local rpc server api access, this allows you to receive notifications pushed out to the user
*
* @remarks
* This scope requires Discord approval to be used
*/
RPCNotificationsRead = 'rpc.notifications.read',
/**
* For local rpc server access, this allows you to read a user's voice settings and listen for voice events
*
* @remarks
* This scope requires Discord approval to be used
*/
RPCVoiceRead = 'rpc.voice.read',
/**
* For local rpc server access, this allows you to update a user's voice settings
*
* @remarks
* This scope requires Discord approval to be used
*/
RPCVoiceWrite = 'rpc.voice.write',
/**
* Allows your app to connect to voice on user's behalf and see all the voice members
*
* @remarks
* This scope requires Discord approval to be used
*/
Voice = 'voice',
/** Generate a webhook that is returned in the oauth token response for authorization code grants */
WebhookIncoming = 'webhook.incoming',
}
/** https://discord.com/developers/docs/resources/guild#integration-object-integration-structure */
export interface DiscordIntegration {
/** Integration Id */
@@ -111,7 +241,7 @@ export interface DiscordIntegration {
/** The bot/OAuth2 application for discord integrations */
application?: DiscordIntegrationApplication
/** the scopes the application has been authorized for */
scopes: string[]
scopes: OAuth2Scope[]
}
/** https://discord.com/developers/docs/resources/guild#integration-account-object-integration-account-structure */
@@ -192,7 +322,7 @@ export interface DiscordMember {
joined_at: string
/** When the user started boosting the guild */
premium_since?: string | null
/** The permissions this member has in the guild. Only present on interaction events. */
/** The permissions this member has in the guild. Only present on interaction events and OAuth2 current member fetch. */
permissions?: string
/** when the user's timeout will expire and the user will be able to communicate in the guild again (set null to remove timeout), null or a time in the past if the user is not timed out */
communication_disabled_until?: string | null
@@ -244,6 +374,149 @@ export interface DiscordApplication {
role_connections_verification_url?: string
}
export type DiscordTokenExchange = DiscordTokenExchangeAuthorizationCode | DiscordTokenExchangeRefreshToken | DiscordTokenExchangeClientCredentials
export interface DiscordTokenExchangeAuthorizationCode {
/** Application's client id */
client_id: BigString
/** application's client secret */
client_secret: string
grant_type: 'authorization_code'
/** The code for the token exchange */
code: string
/** The redirect_uri associated with this authorization */
redirect_uri: string
}
/** https://discord.com/developers/docs/topics/oauth2#client-credentials-grant */
export interface DiscordTokenExchangeRefreshToken {
/** Application's client id */
client_id: BigString
/** application's client secret */
client_secret: string
grant_type: 'refresh_token'
/** the user's refresh token */
refresh_token: string
}
/** https://discord.com/developers/docs/topics/oauth2#client-credentials-grant */
export interface DiscordTokenExchangeClientCredentials {
/** Application's client id */
client_id: BigString
/** application's client secret */
client_secret: string
grant_type: 'client_credentials'
/** The scope(s) for the access token */
scope: OAuth2Scope[]
}
export interface DiscordAccessTokenResponse {
/** The access token of the user */
access_token: string
/** The type of token */
token_type: string
/** The number of seconds after that the access token is expired */
expires_in: number
/**
* The refresh token to refresh the access token
*
* @remarks
* When the token exchange is a client credentials type grant this value is not defined.
*/
refresh_token: string
/** The scopes for the access token */
scope: string
/** The webhook the user created for the application. Requires the `webhook.incoming` scope */
webhook?: DiscordIncomingWebhook
/** The guild the bot has been added. Requires the `bot` scope */
guild?: DiscordGuild
}
export interface DiscordTokenRevocation {
/** Application's client id */
client_id: BigString
/** application's client secret */
client_secret: string
/** The access token to revoke */
token: string
}
/** https://discord.com/developers/docs/topics/oauth2#get-current-authorization-information-response-structure */
export interface DiscordCurrentAuthorization {
application: DiscordApplication
/** the scopes the user has authorized the application for */
scopes: OAuth2Scope[]
/** when the access token expires */
expires: string
/** the user who has authorized, if the user has authorized with the `identify` scope */
user?: DiscordUser
}
/** https://discord.com/developers/docs/resources/user#connection-object-connection-structure */
export interface DiscordConnection {
/** id of the connection account */
id: string
/** the username of the connection account */
name: string
/** the service of this connection */
type: DiscordConnectionServiceType
/** whether the connection is revoked */
revoked?: boolean
/** an array of partial server integrations */
integrations?: Array<Partial<DiscordIntegration>>
/** whether the connection is verified */
verified: boolean
/** whether friend sync is enabled for this connection */
friend_sync: boolean
/** whether activities related to this connection will be shown in presence updates */
show_activity: boolean
/** whether this connection has a corresponding third party OAuth2 token */
two_way_link: boolean
/** visibility of this connection */
visibility: DiscordConnectionVisibility
}
/** https://discord.com/developers/docs/resources/user#connection-object-services */
export enum DiscordConnectionServiceType {
BattleNet = 'battlenet',
eBay = 'ebay',
EpicGames = 'epicgames',
Facebook = 'facebook',
GitHub = 'github',
Instagram = 'instagram',
LeagueOfLegends = 'leagueoflegends',
PayPal = 'paypal',
PlayStationNetwork = 'playstation',
Reddit = 'reddit',
RiotGames = 'riotgames',
Spotify = 'spotify',
Skype = 'skype',
Steam = 'steam',
TikTok = 'tiktok',
Twitch = 'twitch',
Twitter = 'twitter',
Xbox = 'xbox',
YouTube = 'youtube',
}
/** https://discord.com/developers/docs/resources/user#connection-object-visibility-types */
export enum DiscordConnectionVisibility {
/** invisible to everyone except the user themselves */
None = 0,
/** visible to everyone */
Everyone = 1,
}
/** https://discord.com/developers/docs/resources/user#application-role-connection-object-application-role-connection-structure */
export interface DiscordApplicationRoleConnection {
/** the vanity name of the platform a bot has connected (max 50 characters) */
platform_name: string | null
/** the username on the platform a bot has connected (max 100 characters) */
platform_username: string | null
/** object mapping application role connection metadata keys to their stringified value (max 100 characters) for the user on the platform a bot has connected */
metadata: Record<string, string>
}
/** https://discord.com/developers/docs/topics/teams#data-models-team-object */
export interface DiscordTeam {
/** A hash of the image of the team's icon */
@@ -584,6 +857,25 @@ export interface DiscordGuild {
stickers?: DiscordSticker[]
}
export interface DiscordPartialGuild {
/** Guild name (2-100 characters, excluding trailing and leading whitespace) */
name: string
/** Guild id */
id: string
/** Icon hash */
icon: string | null
/** true if the user is the owner of the guild */
owner: boolean
/** total permissions for the user in the guild (excludes overwrites and implicit permissions) */
permissions: string
/** Enabled guild features */
features: GuildFeatures[]
/** Approximate number of members in this guild, returned from the GET /guilds/id endpoint when with_counts is true */
approximate_member_count?: number
/** Approximate number of non-offline members in this guild, returned from the GET /guilds/id endpoint when with_counts is true */
approximate_presence_count?: number
}
/** https://discord.com/developers/docs/topics/permissions#role-object-role-structure */
export interface DiscordRole {
/** Role id */
@@ -2320,7 +2612,7 @@ export interface DiscordGuildWidgetSettings {
export interface DiscordInstallParams {
/** the scopes to add the application to the server with */
scopes: string[]
scopes: OAuth2Scope[]
/** the permissions to request for the bot role */
permissions: string
}

View File

@@ -337,6 +337,18 @@ export interface GetGuildAuditLog {
limit?: number
}
/** https://discord.com/developers/docs/resources/user#get-current-user-guilds-query-string-params */
export interface GetUserGuilds {
/** Get guilds before this guild ID */
before?: BigString
/** Get guilds after this guild ID */
after?: BigString
/** Maximum number of entries (between 1-200) to return, defaults to 200 */
limit?: number
/** Include approximate member and presence counts in response, defaults to false */
withCounts?: boolean
}
export interface GetBans {
/** Number of users to return (up to maximum 1000). Default: 1000 */
limit?: number
@@ -532,6 +544,56 @@ export interface CreateGuildChannel {
defaultSortOrder?: SortOrderTypes | null
}
export interface CreateGlobalApplicationCommandOptions {
/** The bearer token of the developer of the application */
bearerToken: string
}
export interface CreateGuildApplicationCommandOptions {
/** The bearer token of the developer of the application */
bearerToken: string
}
export interface UpsertGlobalApplicationCommandOptions {
/** The bearer token of the developer of the application */
bearerToken: string
}
export interface UpsertGuildApplicationCommandOptions {
/** The bearer token of the developer of the application */
bearerToken: string
}
/** https://discord.com/developers/docs/resources/user#create-group-dm-json-params */
export interface GetGroupDmOptions {
/** Access tokens of users that have granted your app the `gdm.join` scope */
accessTokens: string[]
/** A mapping of user ids to their respective nicknames */
nicks?: Record<string, string>
}
/** https://discord.com/developers/docs/resources/channel#group-dm-add-recipient-json-params */
export interface AddDmRecipientOptions {
/** access token of a user that has granted your app the `gdm.join` scope */
accessToken: string
/** nickname of the user being added */
nick?: string
}
/** https://discord.com/developers/docs/resources/guild#add-guild-member-json-params */
export interface AddGuildMemberOptions {
/** access token of a user that has granted your app the `guilds.join` scope */
accessToken: string
/** Value to set user's nickname to. Requires MANAGE_NICKNAMES permission on the bot */
nick?: string
/** Array of role ids the member is assigned. Requires MANAGE_ROLES permission on the bot */
roles?: string[]
/** Whether the user is muted in voice channels. Requires MUTE_MEMBERS permission on the bot */
mute?: boolean
/** Whether the user is deafened in voice channels. Requires DEAFEN_MEMBERS permission on the bot */
deaf?: boolean
}
export interface ModifyChannel {
/** 1-100 character channel name */
name?: string
@@ -872,6 +934,14 @@ export interface ApplicationCommandPermissions {
permission: boolean
}
/** Additional proprieties for https://discord.com/developers/docs/interactions/application-commands#get-guild-application-command-permissions and https://discord.com/developers/docs/interactions/application-commands#get-guild-application-command-permissions */
export interface GetApplicationCommandPermissionOptions {
/** Access token of the user. Requires the `applications.commands.permissions.update` scope */
accessToken: string
/** Id of the application */
applicationId: BigString
}
/** https://discord.com/developers/docs/resources/guild#create-guild */
export interface CreateGuild {
/** Name of the guild (1-100 characters) */

View File

@@ -1,11 +1,12 @@
export * from './Collection.js'
export * from './base64.js'
export * from './bucket.js'
export * from './casing.js'
export * from './Collection.js'
export * from './colors.js'
export * from './hash.js'
export * from './images.js'
export * from './logger.js'
export * from './oauth2.js'
export * from './permissions.js'
export * from './reactions.js'
export * from './token.js'

View File

@@ -0,0 +1,75 @@
import type { BigString, OAuth2Scope, PermissionStrings } from '@discordeno/types'
import { calculateBits } from './permissions.js'
export function createOAuth2Link(options: CreateOAuth2LinkOptions): string {
const joinedScopeString = options.scope.join(' ')
let url = `https://discord.com/oauth2/authorize?client_id=${options.clientId}&scope=${joinedScopeString}`
if (options.responseType) url += `&response_type=${options.responseType}`
if (options.state) url += `&state=${encodeURIComponent(options.state)}`
if (options.redirectUri) url += `&redirect_uri=${encodeURIComponent(options.redirectUri)}`
if (options.prompt) url += `&prompt=${options.prompt}`
if (options.permissions) url += `&permissions=${Array.isArray(options.permissions) ? calculateBits(options.permissions) : options.permissions}`
if (options.guildId) url += `&guild_id=${options.guildId}`
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
if (options.disableGuildSelect !== undefined) url += `&disable_guild_select=${options.disableGuildSelect}`
return url
}
export interface CreateOAuth2LinkOptions {
/**
* The type of response
*
* @remarks
* Should be defined only if using either OAuth2 authorization, implicit or not, or [advanced bot authorization](https://discord.com/developers/docs/topics/oauth2#advanced-bot-authorization)
*/
responseType?: 'code' | 'token'
/** The id of the application */
clientId: BigString
/** The scopes for the application */
scope: OAuth2Scope[]
/**
* The optional state for security
*
* @see https://discord.com/developers/docs/topics/oauth2#state-and-security
*/
state?: string
/**
* The redirect uri for after the authentication
*
* @remarks
* Should be defined only if using either OAuth2 authorization, implicit or not, or [advanced bot authorization](https://discord.com/developers/docs/topics/oauth2#advanced-bot-authorization)
*/
redirectUri?: string
/**
* The type of prompt to give to the user
*
* @remarks
* If set to `none`, it will skip the authorization screen and redirect them back to your redirect URI without requesting their authorization.
* For passthrough scopes, like bot and webhook.incoming, authorization is always required.
*/
prompt?: 'consent' | 'none'
/**
* The permissions of the invited bot
*
* @remarks
* Should be defined only in a [bot authorization flow](https://discord.com/developers/docs/topics/oauth2#bot-authorization-flow) or with [advanced bot authorization](https://discord.com/developers/docs/topics/oauth2#advanced-bot-authorization)
*/
permissions?: BigString | PermissionStrings[]
/**
* Pre-fills the dropdown picker with a guild for the user
*
* @remarks
* Should be defined only in a [bot authorization flow](https://discord.com/developers/docs/topics/oauth2#bot-authorization-flow) or with [advanced bot authorization](https://discord.com/developers/docs/topics/oauth2#advanced-bot-authorization) or with the `webhook.incoming` scope
*/
guildId?: BigString
/**
* Disallows the user from changing the guild dropdown if set to true
*
* @remarks
* Should be defined only in a [bot authorization flow](https://discord.com/developers/docs/topics/oauth2#bot-authorization-flow), with [advanced bot authorization](https://discord.com/developers/docs/topics/oauth2#advanced-bot-authorization) or with the `webhook.incoming` scope
*/
disableGuildSelect?: boolean
}

View File

@@ -1,15 +1,20 @@
import { Buffer } from 'node:buffer'
/** Removes the Bot before the token. */
const validTokenPrefixes = ['Bot', 'Bearer']
/** Removes the Bot/Bearer before the token. */
export function removeTokenPrefix(token?: string, type: 'GATEWAY' | 'REST' = 'REST'): string {
// If no token is provided, throw an error
if (token === undefined) {
throw new Error(`The ${type} was not given a token. Please provide a token and try again.`)
}
const splittedToken = token.split(' ')
// If the token does not have a prefix just return token
if (!token.startsWith('Bot ')) return token
if (splittedToken.length < 2 || !validTokenPrefixes.includes(splittedToken[0])) return token
// Remove the prefix and return only the token.
return token.substring(token.indexOf(' ') + 1)
return splittedToken.splice(1).join(' ')
}
/** Get the bot id from the bot token. WARNING: Discord staff has mentioned this may not be stable forever. Use at your own risk. However, note for over 5 years this has never broken. */