feat(cli)!: CLI generated types (#3453)

* Move transformers types to a single file

* Add script to test TS Compiler api capabilities
Remove nested objects where possible

* Use Partial<Role> and DiscordOverwrite instead of objects in AuditLogChange

* Fix typescript errors

* Remove interfaces.json

It is a generated file from the test script, not something that should be commited

* Start work on the test generation script
The TS version got bumped to 5.5.3

* Fix any type, use node16 module resolution

"node" is a deprecated alias for "node10", node16 is the recommended

Add code to check for type errors when getting the types from the interfaces

Fix errors when TS tries to find the files that the root tsconfig.json handles

* remove some useless compile options in TSApiTest

* Add jsdoc parsing
JSDoc tags with the next right after are not supported

* fix small format issue

* Fix ApplicationCommandPermissions todo

* Fix CI error

* Simplify parseDocumentation

* get type directly from the sourceFile

* remove hasUndefinedUnion as it is no longer used

* fix discordeno bin file name

* Update the yarn lockfile

* Merge scripts into one

* Use `@internal` instead of `@private` + `@deprecated`

* work on .d.ts files

* Process interface members once

* Move emit/preEmit diagnostics to a unit test

they can take quite a while, even 3-4 seconds, so running in the CLI every time does not seem ideal

* test: add build:types to test:unit dependencies

This is a test commit, it will provably get reversed. The scope is to see what CI does with this

* add find-up to deno import map

* add typescript to the deno import map

* Add node:assert to deno import map

* check for this.timeout that exists

deno does not have the timeout

* add build:type to deno-unit

* Temp hack to work-around the script deno tests

* Test with bun running the test as well

* fix turbo deps for unit tests

* remove test scripts

* Update CI to use build:type cache

* Apply code review suggestion

Co-authored-by: LTS20050703 <lts20050703@gmail.com>

* Add code to modify the interface member

* use an actual config

The config source is still a dummy object for now

* Search and use discordeno.config.js file

* provide config from cli options

* Handle props in base objects
The dependencies of an object need to be declared/updated manually, it would be painful to fix this in such a way that would be something that Typescript can give us from the compiler API

* add some tests for desired proprieties

* also check for a .mjs config file

* Add support for .ts config files

We use the typescript compiler to emit an in-memory version of the config file, import it and then remove it. It does not do bundling so if the config imports local files it will not work. Also the file is not type-checked as it would slow down the config loading

* remove compiler host, use callback on program.emit

* fix deno ci error

* add node:fs/promises to deno import map

---------

Co-authored-by: LTS20050703 <lts20050703@gmail.com>
This commit is contained in:
Fleny
2024-07-20 23:52:21 +02:00
committed by GitHub
parent c3a004ee40
commit 27fc12ec34
88 changed files with 3496 additions and 2849 deletions

View File

@@ -135,7 +135,7 @@ jobs:
# Not using matrix because test later on cant needs a specific job
bot-unit-test:
name: Bot
needs: build-dist
needs: [build-dist, build-type-and-test]
uses: ./.github/workflows/unit-test.yml
with:
package: bot
@@ -157,7 +157,7 @@ jobs:
discordeno-unit-test:
name: Discordeno
needs: build-dist
needs: [build-dist, build-type-and-test]
uses: ./.github/workflows/unit-test.yml
with:
package: discordeno
@@ -170,7 +170,7 @@ jobs:
gateway-unit-test:
name: Gateway
needs: build-dist
needs: [build-dist, build-type-and-test]
uses: ./.github/workflows/unit-test.yml
with:
package: gateway
@@ -189,7 +189,7 @@ jobs:
rest-unit-test:
name: Rest
needs: build-dist
needs: [build-dist, build-type-and-test]
uses: ./.github/workflows/unit-test.yml
with:
package: rest
@@ -211,7 +211,7 @@ jobs:
types-unit-test:
name: Types
needs: build-dist
needs: [build-dist, build-type-and-test]
uses: ./.github/workflows/unit-test.yml
with:
package: types
@@ -224,7 +224,7 @@ jobs:
utils-unit-test:
name: Utils
needs: build-dist
needs: [build-dist, build-type-and-test]
uses: ./.github/workflows/unit-test.yml
with:
package: utils

View File

@@ -41,6 +41,12 @@ jobs:
with:
path: .turbo
key: ${{ runner.os }}-turbo-build-${{ github.sha }}
- name: Build type cache
if: steps.turbo-cache.outputs.cache-hit != 'true'
uses: actions/cache@v3
with:
path: .turbo
key: ${{ runner.os }}-turbo-build:type-${{ github.sha }}
- name: Deno cache
uses: actions/cache@v3
with:
@@ -85,6 +91,12 @@ jobs:
with:
path: .turbo
key: ${{ runner.os }}-turbo-build-${{ github.sha }}
- name: Build type cache
if: steps.turbo-cache.outputs.cache-hit != 'true'
uses: actions/cache@v3
with:
path: .turbo
key: ${{ runner.os }}-turbo-build:type-${{ github.sha }}
- name: Bun Unit Test
run: yarn test:bun-unit --cache-dir=".turbo" --filter=./packages/${{ inputs.package }}
timeout-minutes: 1

View File

@@ -38,6 +38,12 @@ jobs:
with:
path: .turbo
key: ${{ runner.os }}-turbo-build-${{ github.sha }}
- name: Build type cache
if: steps.turbo-cache.outputs.cache-hit != 'true'
uses: actions/cache@v3
with:
path: .turbo
key: ${{ runner.os }}-turbo-build:type-${{ github.sha }}
- name: Unit Test
run: yarn test:unit --cache-dir=".turbo" --filter=./packages/${{ inputs.package }}
timeout-minutes: 1

View File

@@ -2,16 +2,22 @@
"imports": {
"node:buffer": "https://deno.land/std@0.176.0/node/buffer.ts",
"node:fs": "https://deno.land/std@0.176.0/node/fs.ts",
"node:fs/promises": "https://deno.land/std@0.176.0/node/fs/promises.ts",
"node:events": "https://deno.land/std@0.176.0/node/events.ts",
"tweetnacl": "npm:tweetnacl",
"ws": "npm:ws",
"mocha": "https://deno.land/std@0.168.0/testing/bdd.ts",
"chai": "https://cdn.skypack.dev/chai@4.3.4?dts",
"sinon": "https://cdn.skypack.dev/sinon@15.0.0?dts",
"find-up": "npm:find-up",
"typescript": "npm:typescript",
"chai-as-promised": "npm:chai-as-promised",
"benchmark": "npm:benchmark",
"dotenv": "npm:dotenv",
"node:zlib": "https://deno.land/std@0.176.0/node/zlib.ts",
"node:assert": "https://deno.land/std@0.176.0/node/assert.ts",
"node:path": "https://deno.land/std@0.176.0/node/path.ts",
"node:url": "https://deno.land/std@0.176.0/node/url.ts",
"@discordeno/utils": "./packages/utils/dist/esm/index.js",
"@discordeno/types": "./packages/types/dist/esm/index.js",
"@discordeno/rest": "./packages/rest/dist/esm/index.js",

View File

@@ -29,6 +29,7 @@
"devDependencies": {
"@biomejs/biome": "^1.8.0",
"chokidar-cli": "^3.0.0",
"discordeno": "19.0.0-alpha.1",
"husky": "^9.0.11",
"lint-staged": "^15.2.7",
"turbo": "^2.0.6",

View File

@@ -40,18 +40,18 @@ bot.transformers.desiredProperties.message.editedTimestamp = true
bot.transformers.desiredProperties.message.embeds = true
bot.transformers.desiredProperties.message.guildId = true
bot.transformers.desiredProperties.message.id = true
bot.transformers.desiredProperties.message.interaction.id = true
bot.transformers.desiredProperties.message.interaction.member = true
bot.transformers.desiredProperties.message.interaction.name = true
bot.transformers.desiredProperties.message.interaction.user = true
bot.transformers.desiredProperties.message.interaction.type = true
bot.transformers.desiredProperties.messageInteraction.id = true
bot.transformers.desiredProperties.messageInteraction.member = true
bot.transformers.desiredProperties.messageInteraction.name = true
bot.transformers.desiredProperties.messageInteraction.user = true
bot.transformers.desiredProperties.messageInteraction.type = true
bot.transformers.desiredProperties.message.member = true
bot.transformers.desiredProperties.message.mentionedChannelIds = true
bot.transformers.desiredProperties.message.mentionedRoleIds = true
bot.transformers.desiredProperties.message.mentions = true
bot.transformers.desiredProperties.message.messageReference.messageId = true
bot.transformers.desiredProperties.message.messageReference.channelId = true
bot.transformers.desiredProperties.message.messageReference.guildId = true
bot.transformers.desiredProperties.messageReference.messageId = true
bot.transformers.desiredProperties.messageReference.channelId = true
bot.transformers.desiredProperties.messageReference.guildId = true
bot.transformers.desiredProperties.message.nonce = true
bot.transformers.desiredProperties.message.reactions = true
bot.transformers.desiredProperties.message.stickerItems = true

View File

@@ -7,26 +7,28 @@ import { type Collection, createLogger, getBotIdFromToken, type logger } from '@
import { createBotGatewayHandlers } from './handlers.js'
import { type BotHelpers, createBotHelpers } from './helpers.js'
import { type Transformers, createTransformers } from './transformers.js'
import type { ApplicationCommandPermission } from './transformers/applicationCommandPermission.js'
import type { AuditLogEntry } from './transformers/auditLogEntry.js'
import type { AutoModerationActionExecution } from './transformers/automodActionExecution.js'
import type { AutoModerationRule } from './transformers/automodRule.js'
import type { Channel } from './transformers/channel.js'
import type { Emoji } from './transformers/emoji.js'
import type { Entitlement } from './transformers/entitlement.js'
import type { Guild } from './transformers/guild.js'
import type { Integration } from './transformers/integration.js'
import type { Interaction } from './transformers/interaction.js'
import type { Invite } from './transformers/invite.js'
import type { Member } from './transformers/member.js'
import type { Message } from './transformers/message.js'
import type { PresenceUpdate } from './transformers/presence.js'
import type { Role } from './transformers/role.js'
import type { ScheduledEvent } from './transformers/scheduledEvent.js'
import type { Sticker } from './transformers/sticker.js'
import type { ThreadMember } from './transformers/threadMember.js'
import type { User } from './transformers/user.js'
import type { VoiceState } from './transformers/voiceState.js'
import type {
AuditLogEntry,
AutoModerationActionExecution,
AutoModerationRule,
Channel,
Emoji,
Entitlement,
Guild,
GuildApplicationCommandPermissions,
Integration,
Interaction,
Invite,
Member,
Message,
PresenceUpdate,
Role,
ScheduledEvent,
Sticker,
ThreadMember,
User,
VoiceState,
} from './transformers/index.js'
import type { BotGatewayHandlerOptions } from './typings.js'
/**
@@ -161,7 +163,7 @@ export interface Bot {
export interface EventHandlers {
debug: (text: string, ...args: any[]) => unknown
applicationCommandPermissionsUpdate: (command: ApplicationCommandPermission) => unknown
applicationCommandPermissionsUpdate: (command: GuildApplicationCommandPermissions) => unknown
guildAuditLogEntryCreate: (log: AuditLogEntry, guildId: bigint) => unknown
automodRuleCreate: (rule: AutoModerationRule) => unknown
automodRuleUpdate: (rule: AutoModerationRule) => unknown

View File

@@ -2,11 +2,11 @@ import type { CreateWebhook } from '@discordeno/rest'
import type {
AddDmRecipientOptions,
AddGuildMemberOptions,
ApplicationCommandPermissions,
AtLeastOne,
BeginGuildPrune,
BigString,
CamelizedDiscordAccessTokenResponse,
CamelizedDiscordApplicationCommandPermissions,
CamelizedDiscordApplicationRoleConnection,
CamelizedDiscordArchivedThreads,
CamelizedDiscordAuditLog,
@@ -94,31 +94,34 @@ import type {
} from '@discordeno/types'
import { snakelize } from '@discordeno/utils'
import type { Bot } from './bot.js'
import type { Application } from './transformers/application.js'
import type { ApplicationCommand } from './transformers/applicationCommand.js'
import type { ApplicationCommandPermission } from './transformers/applicationCommandPermission.js'
import type { AutoModerationRule } from './transformers/automodRule.js'
import type { Channel } from './transformers/channel.js'
import type { Emoji } from './transformers/emoji.js'
import { type Entitlement } from './transformers/entitlement.js'
import type { Guild } from './transformers/guild.js'
import type { Integration } from './transformers/integration.js'
import type { Invite } from './transformers/invite.js'
import type { Member } from './transformers/member.js'
import type { Message } from './transformers/message.js'
import type { GuildOnboarding } from './transformers/onboarding.js'
import type { Role } from './transformers/role.js'
import type { ScheduledEvent } from './transformers/scheduledEvent.js'
import type { Sku } from './transformers/sku.js'
import type { StageInstance } from './transformers/stageInstance.js'
import type { Sticker, StickerPack } from './transformers/sticker.js'
import type { Template } from './transformers/template.js'
import type { ThreadMember } from './transformers/threadMember.js'
import type { User } from './transformers/user.js'
import type { Webhook } from './transformers/webhook.js'
import type { WelcomeScreen } from './transformers/welcomeScreen.js'
import type { GuildWidget } from './transformers/widget.js'
import type { GuildWidgetSettings } from './transformers/widgetSettings.js'
import type {
Application,
ApplicationCommand,
AutoModerationRule,
Channel,
Emoji,
Entitlement,
Guild,
GuildApplicationCommandPermissions,
GuildOnboarding,
GuildWidget,
GuildWidgetSettings,
Integration,
Invite,
Member,
Message,
Role,
ScheduledEvent,
Sku,
StageInstance,
Sticker,
StickerPack,
Template,
ThreadMember,
User,
Webhook,
WelcomeScreen,
} from './transformers/index.js'
export function createBotHelpers(bot: Bot): BotHelpers {
return {
@@ -740,8 +743,8 @@ export interface BotHelpers {
guildId: BigString,
commandId: BigString,
bearerToken: string,
options: ApplicationCommandPermissions[],
) => Promise<ApplicationCommandPermission>
options: CamelizedDiscordApplicationCommandPermissions[],
) => Promise<GuildApplicationCommandPermissions>
editAutomodRule: (
guildId: BigString,
ruleId: BigString,
@@ -791,8 +794,11 @@ export interface BotHelpers {
guildId: BigString,
commandId: BigString,
options?: GetApplicationCommandPermissionOptions,
) => Promise<ApplicationCommandPermission>
getApplicationCommandPermissions: (guildId: BigString, options?: GetApplicationCommandPermissionOptions) => Promise<ApplicationCommandPermission[]>
) => Promise<GuildApplicationCommandPermissions>
getApplicationCommandPermissions: (
guildId: BigString,
options?: GetApplicationCommandPermissionOptions,
) => Promise<GuildApplicationCommandPermissions[]>
getAuditLog: (guildId: BigString, options?: GetGuildAuditLog) => Promise<CamelizedDiscordAuditLog>
getAutomodRule: (guildId: BigString, ruleId: BigString) => Promise<AutoModerationRule>
getAutomodRules: (guildId: BigString) => Promise<AutoModerationRule[]>

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import type { ActivityTypes, Bot, DiscordActivity } from '../index.js'
import type { Activity, Bot, DiscordActivity } from '../index.js'
export function transformActivity(bot: Bot, payload: DiscordActivity): Activity {
const activity = {
@@ -35,36 +35,3 @@ export function transformActivity(bot: Bot, payload: DiscordActivity): Activity
return bot.transformers.customizers.activity(bot, payload, activity)
}
export interface Activity {
join?: string
flags?: number
applicationId?: bigint
spectate?: string
url?: string
startedAt?: number
endedAt?: number
details?: string
state?: string
emoji?: {
id?: bigint
animated?: boolean
name: string
}
partyId?: string
partyCurrentSize?: number
partyMaxSize?: number
largeImage?: string
largeText?: string
smallImage?: string
smallText?: string
match?: string
instance?: boolean
buttons?: Array<{
url: string
label: string
}>
name: string
type: ActivityTypes
createdAt: number
}

View File

@@ -1,13 +1,9 @@
import {
type ApplicationFlags,
type Application,
type Bot,
type DiscordApplication,
DiscordApplicationIntegrationType,
type DiscordUser,
type Guild,
type OAuth2Scope,
type Team,
type User,
iconHashToBigInt,
} from '../index.js'
@@ -64,39 +60,3 @@ export function transformApplication(bot: Bot, payload: { application: DiscordAp
return bot.transformers.customizers.application(bot, payload.application, application)
}
export interface Application {
flags?: ApplicationFlags
icon?: bigint
rpcOrigins?: string[]
termsOfServiceUrl?: string
privacyPolicyUrl?: string
primarySkuId?: string
slug?: string
coverImage?: bigint
owner?: User
team?: Team
guildId?: bigint
guild?: Guild
id: bigint
name: string
description: string
botPublic: boolean
botRequireCodeGrant: boolean
verifyKey: string
approximateGuildCount?: number
bot?: User
redirectUris?: string[]
interactionsEndpointUrl?: string
integrationTypesConfig?: Partial<Record<DiscordApplicationIntegrationType, ApplicationIntegrationTypeConfiguration>>
}
export interface ApplicationIntegrationTypeConfiguration {
/** Install params for each installation context's default in-app authorization link */
oauth2InstallParams?: {
/** Scopes to add the application to the server with */
scopes: OAuth2Scope[]
/** Permissions to request for the bot role */
permissions: bigint
}
}

View File

@@ -1,5 +1,5 @@
import type { ApplicationCommandTypes, DiscordApplicationCommand, Locales } from '@discordeno/types'
import type { ApplicationCommandOption, Bot } from '../index.js'
import type { DiscordApplicationCommand } from '@discordeno/types'
import type { ApplicationCommand, Bot } from '../index.js'
export function transformApplicationCommand(bot: Bot, payload: DiscordApplicationCommand): ApplicationCommand {
const applicationCommand = {
@@ -20,18 +20,3 @@ export function transformApplicationCommand(bot: Bot, payload: DiscordApplicatio
return bot.transformers.customizers.applicationCommand(bot, payload, applicationCommand)
}
export interface ApplicationCommand {
options?: ApplicationCommandOption[]
description?: string
guildId?: bigint
nameLocalizations?: Record<Locales, string>
descriptionLocalizations?: Record<Locales, string>
defaultMemberPermissions?: bigint
type?: ApplicationCommandTypes
version?: string
id: bigint
name: string
applicationId: bigint
dmPermission: boolean
}

View File

@@ -1,6 +1,5 @@
import type { ApplicationCommandOptionTypes, ChannelTypes, DiscordApplicationCommandOption, Localization } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { ApplicationCommandOptionChoice } from './applicationCommandOptionChoice.js'
import type { DiscordApplicationCommandOption } from '@discordeno/types'
import type { ApplicationCommandOption, Bot } from '../index.js'
export function transformApplicationCommandOption(bot: Bot, payload: DiscordApplicationCommandOption): ApplicationCommandOption {
const applicationCommandOption = {
@@ -22,34 +21,3 @@ export function transformApplicationCommandOption(bot: Bot, payload: DiscordAppl
return bot.transformers.customizers.applicationCommandOption(bot, payload, applicationCommandOption)
}
export interface ApplicationCommandOption {
/** Value of Application Command Option Type */
type: ApplicationCommandOptionTypes
/** 1-32 character name matching lowercase `^[\w-]{1,32}$` */
name: string
/** Localization object for the `name` field. Values follow the same restrictions as `name` */
nameLocalizations?: Localization
/** 1-100 character description */
description: string
/** Localization object for the `description` field. Values follow the same restrictions as `description` */
descriptionLocalizations?: Localization
/** If the parameter is required or optional--default `false` */
required?: boolean
/** Choices for `string` and `int` types for the user to pick from */
choices?: ApplicationCommandOptionChoice[]
/** If the option is a subcommand or subcommand group type, this nested options will be the parameters */
options?: ApplicationCommandOption[]
/** If the option is a channel type, the channels shown will be restricted to these types */
channelTypes?: ChannelTypes[]
/** Minimum number desired. */
minValue?: number
/** Maximum number desired. */
maxValue?: number
/** Minimum length desired. */
minLength?: number
/** Maximum length desired. */
maxLength?: number
/** if autocomplete interactions are enabled for this `String`, `Integer`, or `Number` type option */
autocomplete?: boolean
}

View File

@@ -1,5 +1,5 @@
import type { DiscordApplicationCommandOptionChoice, Locales } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { DiscordApplicationCommandOptionChoice } from '@discordeno/types'
import type { ApplicationCommandOptionChoice, Bot } from '../index.js'
export function transformApplicationCommandOptionChoice(bot: Bot, payload: DiscordApplicationCommandOptionChoice): ApplicationCommandOptionChoice {
const applicationCommandOptionChoice = {
@@ -10,9 +10,3 @@ export function transformApplicationCommandOptionChoice(bot: Bot, payload: Disco
return bot.transformers.customizers.applicationCommandOptionChoice(bot, payload, applicationCommandOptionChoice)
}
export interface ApplicationCommandOptionChoice {
nameLocalizations?: Record<Locales, string>
name: string
value: string | number
}

View File

@@ -1,7 +1,10 @@
import type { ApplicationCommandPermissionTypes, DiscordGuildApplicationCommandPermissions } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { DiscordGuildApplicationCommandPermissions } from '@discordeno/types'
import type { Bot, GuildApplicationCommandPermissions } from '../index.js'
export function transformApplicationCommandPermission(bot: Bot, payload: DiscordGuildApplicationCommandPermissions): ApplicationCommandPermission {
export function transformApplicationCommandPermission(
bot: Bot,
payload: DiscordGuildApplicationCommandPermissions,
): GuildApplicationCommandPermissions {
const applicationCommandPermission = {
id: bot.transformers.snowflake(payload.id),
applicationId: bot.transformers.snowflake(payload.application_id),
@@ -11,18 +14,7 @@ export function transformApplicationCommandPermission(bot: Bot, payload: Discord
type: perm.type,
permission: perm.permission,
})),
} as ApplicationCommandPermission
} as GuildApplicationCommandPermissions
return bot.transformers.customizers.applicationCommandPermission(bot, payload, applicationCommandPermission)
}
export interface ApplicationCommandPermission {
id: bigint
guildId: bigint
applicationId: bigint
permissions: Array<{
id: bigint
type: ApplicationCommandPermissionTypes
permission: boolean
}>
}

View File

@@ -1,5 +1,5 @@
import type { AttachmentFlags, DiscordAttachment } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { DiscordAttachment } from '@discordeno/types'
import type { Attachment, Bot } from '../index.js'
export function transformAttachment(bot: Bot, payload: DiscordAttachment): Attachment {
const props = bot.transformers.desiredProperties.attachment
@@ -22,38 +22,3 @@ export function transformAttachment(bot: Bot, payload: DiscordAttachment): Attac
return bot.transformers.customizers.attachment(bot, payload, attachment)
}
export interface Attachment {
/** Name of file attached */
filename: string
/** The title of the file */
title?: string
/** The attachment's [media type](https://en.wikipedia.org/wiki/Media_type) */
contentType?: string
/** Size of file in bytes */
size: number
/** Source url of file */
url: string
/** A proxied url of file */
proxyUrl: string
/** Attachment id */
id: bigint
/** description for the file (max 1024 characters) */
description?: string
/** Height of file (if image) */
height?: number
/** Width of file (if image) */
width?: number
/**
* whether this attachment is ephemeral.
* Ephemeral attachments will automatically be removed after a set period of time.
* Ephemeral attachments on messages are guaranteed to be available as long as the message itself exists.
*/
ephemeral?: boolean
/** The duration of the audio file for a voice message */
duration_secs?: number
/** A base64 encoded bytearray representing a sampled waveform for a voice message */
waveform?: string
/** Attachment flags combined as a bitfield */
flags?: AttachmentFlags
}

View File

@@ -1,5 +1,5 @@
import type { AuditLogEvents, DiscordAuditLogEntry, OverwriteTypes } from '@discordeno/types'
import { type Bot, iconHashToBigInt } from '../index.js'
import type { DiscordAuditLogEntry } from '@discordeno/types'
import { type AuditLogEntry, type Bot, iconHashToBigInt } from '../index.js'
export function transformAuditLogEntry(bot: Bot, payload: DiscordAuditLogEntry): AuditLogEntry {
const auditLogEntry = {
@@ -136,123 +136,3 @@ export function transformAuditLogEntry(bot: Bot, payload: DiscordAuditLogEntry):
return bot.transformers.customizers.auditLogEntry(bot, payload, auditLogEntry)
}
export interface AuditLogEntry {
id: bigint
userId?: bigint
reason?: string
changes?: Array<{
new?:
| string
| number
| bigint
| boolean
| Array<{
allow?: string
deny?: string
id: string
type: OverwriteTypes
}>
| Array<{
id?: bigint
name?: string
}>
old?:
| string
| number
| bigint
| boolean
| Array<{
allow?: string
deny?: string
id: string
type: OverwriteTypes
}>
| Array<{
id?: bigint
name?: string
}>
key:
| 'id'
| 'name'
| 'description'
| 'type'
| 'permissions'
| 'locked'
| 'invitable'
| 'nsfw'
| 'archived'
| 'position'
| 'topic'
| 'bitrate'
| 'default_auto_archive_duration'
| 'auto_archive_duration'
| 'allow'
| 'deny'
| 'channel_id'
| 'deaf'
| 'mute'
| 'status'
| 'nick'
| 'communication_disabled_until'
| 'color'
| 'permission_overwrites'
| 'user_limit'
| 'rate_limit_per_user'
| 'owner_id'
| 'application_id'
| 'hoist'
| 'mentionable'
| 'location'
| 'verification_level'
| 'default_message_notifications'
| 'explicit_content_filter'
| 'preferred_locale'
| 'afk_timeout'
| 'afk_channel_id'
| 'system_channel_id'
| 'widget_enabled'
| 'mfa_level'
| 'vanity_url_code'
| 'icon_hash'
| 'widget_channel_id'
| 'rules_channel_id'
| 'public_updates_channel_id'
| 'code'
| 'region'
| 'privacy_level'
| 'entity_type'
| 'enable_emoticons'
| 'expire_behavior'
| 'expire_grace_period'
| 'uses'
| 'max_uses'
| 'max_age'
| 'temporary'
| 'discovery_splash_hash'
| 'banner_hash'
| 'image_hash'
| 'splash_hash'
| 'inviter_id'
| 'avatar_hash'
| 'command_id'
| 'prune_delete_days'
| '$add'
| '$remove'
}>
targetId?: bigint
actionType: AuditLogEvents
options?: {
id?: bigint
channelId?: bigint
messageId?: bigint
type: number
count: number
deleteMemberDays: number
membersRemoved: number
roleName: string
autoModerationRuleName: string
autoModerationRuleTriggerType: string
integrationType: string
}
}

View File

@@ -1,5 +1,5 @@
import type { AutoModerationActionType, AutoModerationTriggerTypes, DiscordAutoModerationActionExecution } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { DiscordAutoModerationActionExecution } from '@discordeno/types'
import type { AutoModerationActionExecution, Bot } from '../index.js'
export function transformAutoModerationActionExecution(bot: Bot, payload: DiscordAutoModerationActionExecution): AutoModerationActionExecution {
const rule = {
@@ -25,24 +25,3 @@ export function transformAutoModerationActionExecution(bot: Bot, payload: Discor
return bot.transformers.customizers.automodActionExecution(bot, payload, rule)
}
export interface AutoModerationActionExecution {
channelId?: bigint
messageId?: bigint
alertSystemMessageId?: bigint
guildId: bigint
userId: bigint
content: string
action: {
type: AutoModerationActionType
metadata: {
customMessage?: string
durationSeconds?: number
channelId?: bigint
}
}
ruleTriggerType: AutoModerationTriggerTypes
ruleId: bigint
matchedKeyword: string
matchedContent: string
}

View File

@@ -1,11 +1,5 @@
import type {
AutoModerationActionType,
AutoModerationEventTypes,
AutoModerationTriggerTypes,
DiscordAutoModerationRule,
DiscordAutoModerationRuleTriggerMetadataPresets,
} from '@discordeno/types'
import type { Bot } from '../index.js'
import type { DiscordAutoModerationRule } from '@discordeno/types'
import type { AutoModerationRule, Bot } from '../index.js'
export function transformAutoModerationRule(bot: Bot, payload: DiscordAutoModerationRule): AutoModerationRule {
const rule = {
@@ -41,31 +35,3 @@ export function transformAutoModerationRule(bot: Bot, payload: DiscordAutoModera
return bot.transformers.customizers.automodRule(bot, payload, rule)
}
export interface AutoModerationRule {
triggerMetadata?: {
keywordFilter?: string[]
presets?: DiscordAutoModerationRuleTriggerMetadataPresets[]
allowList?: string[]
mentionTotalLimit?: number
regexPatterns: string[]
}
id: bigint
name: string
guildId: bigint
eventType: AutoModerationEventTypes
triggerType: AutoModerationTriggerTypes
enabled: boolean
creatorId: bigint
exemptRoles: bigint[]
exemptChannels: bigint[]
actions: Array<{
type: AutoModerationActionType
metadata?: {
channelId?: bigint
customMessage?: string
durationSeconds?: number
}
}>
}

View File

@@ -1,4 +1,4 @@
import { type Bot, type DiscordAvatarDecorationData, iconHashToBigInt } from '../index.js'
import { type AvatarDecorationData, type Bot, type DiscordAvatarDecorationData, iconHashToBigInt } from '../index.js'
export function transformAvatarDecorationData(bot: Bot, payload: DiscordAvatarDecorationData): AvatarDecorationData {
const data = {} as AvatarDecorationData
@@ -9,10 +9,3 @@ export function transformAvatarDecorationData(bot: Bot, payload: DiscordAvatarDe
return data
}
export interface AvatarDecorationData {
/** the avatar decoration hash */
asset: bigint
/** id of the avatar decoration's SKU */
skuId: bigint
}

View File

@@ -1,14 +1,5 @@
import type {
BigString,
ChannelTypes,
DiscordChannel,
DiscordForumTag,
ForumLayout,
OverwriteReadable,
SortOrderTypes,
VideoQualityModes,
} from '@discordeno/types'
import { type Bot, type DefaultReactionEmoji, type ThreadMember, type User, calculatePermissions, iconHashToBigInt } from '../index.js'
import type { BigString, DiscordChannel, DiscordForumTag } from '@discordeno/types'
import { type Bot, type Channel, type ForumTag, calculatePermissions, iconHashToBigInt } from '../index.js'
import { Permissions } from './toggles/Permissions.js'
import { ChannelToggles } from './toggles/channel.js'
@@ -29,7 +20,7 @@ export function separateOverwrites(v: bigint): [number, bigint, bigint, bigint]
return [Number(unpack64(v, 3)), unpack64(v, 2), unpack64(v, 0), unpack64(v, 1)] as [number, bigint, bigint, bigint]
}
export const baseChannel: Partial<Channel> & BaseChannel = {
export const baseChannel = {
get archived() {
return !!this.toggles?.archived
},
@@ -66,12 +57,12 @@ export const baseChannel: Partial<Channel> & BaseChannel = {
archiveTimestamp: this.internalThreadMetadata?.archiveTimestamp,
createTimestamp: this.internalThreadMetadata?.createTimestamp,
autoArchiveDuration: this.internalThreadMetadata?.autoArchiveDuration,
locked: this.locked,
invitable: this.invitable,
archived: this.archived,
locked: !!this.toggles?.locked,
invitable: !!this.toggles?.invitable,
archived: !!this.toggles?.archived,
}
},
}
} as Channel
export function transformChannel(bot: Bot, payload: { channel: DiscordChannel } & { guildId?: BigString }): Channel {
const channel = Object.create(baseChannel) as Channel
@@ -142,133 +133,3 @@ export function transformForumTag(bot: Bot, payload: DiscordForumTag): ForumTag
return bot.transformers.customizers.forumTag(bot, payload, forumTag)
}
export interface BaseChannel {
/** Whether the channel is nsfw */
nsfw: boolean
/** Thread-specific fields not needed by other channels */
threadMetadata?: {
/** Timestamp when the thread's archive status was last changed, used for calculating recent activity */
archiveTimestamp?: number
/** Timestamp when the thread was created; only populated for threads created after 2022-01-09 */
createTimestamp?: number
/** Duration in minutes to automatically archive the thread after recent activity */
autoArchiveDuration?: 60 | 1440 | 4320 | 10080
/** When a thread is locked, only users with `MANAGE_THREADS` can unarchive it */
locked: boolean
/** whether non-moderators can add other non-moderators to a thread; only available on private threads */
invitable: boolean
/** Whether the thread is archived */
archived: boolean
}
/** When a thread is created this will be true on that channel payload for the thread. */
newlyCreated: boolean
/** When a thread is locked, only users with `MANAGE_THREADS` can unarchive it */
locked: boolean
/** whether non-moderators can add other non-moderators to a thread; only available on private threads */
invitable: boolean
/** Whether the thread is archived */
archived: boolean
/** for group DM channels: whether the channel is managed by an application via the `gdm.join` OAuth2 scope */
managed: boolean
/** Explicit permission overwrites for members and roles. */
permissionOverwrites: OverwriteReadable[]
}
export interface Channel extends BaseChannel {
/** The id of the channel */
id: bigint
/** The compressed form of all the boolean values on this channel. */
toggles: ChannelToggles
/** The type of channel */
type: ChannelTypes
/** The id of the guild */
guildId?: bigint
/** Sorting position of the channel */
position?: number
/** The name of the channel (1-100 characters) */
name?: string
/** The channel topic (0-4096 characters for GUILD_FORUM channels, 0-1024 characters for all others) */
topic?: string
/** The id of the last message sent in this channel (may not point to an existing or valid message) */
lastMessageId?: bigint
/** The bitrate (in bits) of the voice or stage channel */
bitrate?: number
/** The user limit of the voice or stage channel */
userLimit?: number
/** Amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission `manage_messages` or `manage_channel`, are unaffected */
rateLimitPerUser?: number
/** Id of the creator of the thread */
ownerId?: bigint
/** For guild channels: Id of the parent category for a channel (each parent category can contain up to 50 channels), for threads: id of the text channel this thread was created */
parentId?: bigint
/** When the last pinned message was pinned. This may be null in events such as GUILD_CREATE when a message is not pinned. */
lastPinTimestamp?: number
/** Voice region id for the voice or stage channel, automatic when set to null */
rtcRegion?: string
/** The camera video quality mode of the voice channel, 1 when not present */
videoQualityMode?: VideoQualityModes
/** An approximate count of messages in a thread, stops counting at 50 */
messageCount?: number
/** An approximate count of users in a thread, stops counting at 50 */
memberCount?: number
/**
* Thread-specific fields not needed by other channels.
* @deprecated Use channel.threadMetadata
* @private This field is an internal field, subject to breaking changes.
*/
internalThreadMetadata?: {
/** Timestamp when the thread's archive status was last changed, used for calculating recent activity */
archiveTimestamp: number
/** Timestamp when the thread was created; only populated for threads created after 2022-01-09 */
createTimestamp?: number
/** Duration in minutes to automatically archive the thread after recent activity */
autoArchiveDuration: 60 | 1440 | 4320 | 10080
}
/** Thread member object for the current user, if they have joined the thread, only included on certain API endpoints */
member?: ThreadMember
/** Default duration for newly created threads, in minutes, to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */
defaultAutoArchiveDuration?: number
/** computed permissions for the invoking user in the channel, including overwrites, only included when part of the resolved data received on a slash command interaction. This does not include implicit permissions, which may need to be checked separately. */
permissions?: Permissions
/** The flags of the channel */
flags?: number
/**
* Explicit permission overwrites for members and roles
* @deprecated Use channel.permissionOverwrites
* @private This is for internal use only, and prone to breaking changes.
*/
internalOverwrites?: bigint[]
/** The recipients of a group dm */
recipients?: User[]
/** Icon hash of the group dm */
icon?: bigint
/** Application id of the group DM creator if it is bot-created */
applicationId?: bigint
/** Number of messages ever sent in a thread, it's similar to `message_count` on message creation, but will not decrement the number when a message is deleted */
totalMessageSent?: number
/** The set of tags that can be used in a `GUILD_FORUM` or a `GUILD_MEDIA` channel */
availableTags?: ForumTag[]
/** The IDs of the set of tags that have been applied to a thread in a `GUILD_FORUM` or a `GUILD_MEDIA` channel */
appliedTags?: bigint[]
/** The emoji to show in the add reaction button on a thread in a `GUILD_FORUM` or a `GUILD_MEDIA` channel */
defaultReactionEmoji?: DefaultReactionEmoji
/** the initial `rateLimitPerUser` to set on newly created threads in a channel. this field is copied to the thread at creation time and does not live update. */
defaultThreadRateLimitPerUser?: number
/** The default sort order type used to order posts in `GUILD_FORUM` and `GUILD_MEDIA` channels. Defaults to null, which indicates a preferred sort order hasn't been set by a channel admin */
defaultSortOrder?: SortOrderTypes | null
defaultForumLayout?: ForumLayout
}
export interface ForumTag {
/** The id of the tag */
id: bigint
/** The name of the tag (0-20 characters) */
name: string
/** Whether this tag can only be added to or removed from threads by a member with the MANAGE_THREADS permission */
moderated: boolean
/** The id of a guild's custom emoji At most one of emoji_id and emoji_name may be set. */
emojiId: bigint
/** The unicode character of the emoji */
emojiName: string | null
}

View File

@@ -1,5 +1,4 @@
import type { ButtonStyles, ChannelTypes, MessageComponentTypes, SelectOption, TextStyles } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { Bot, Component } from '../index.js'
import type { DiscordComponent } from '../typings.js'
export function transformComponent(bot: Bot, payload: DiscordComponent): Component {
@@ -42,47 +41,3 @@ export function transformComponent(bot: Bot, payload: DiscordComponent): Compone
return bot.transformers.customizers.component(bot, payload, component)
}
export interface Component {
/** component type */
type: MessageComponentTypes
/** a developer-defined identifier for the component, max 100 characters */
customId?: string
/** whether this component is required to be filled, default true */
required?: boolean
/** whether the component is disabled, default false */
disabled?: boolean
/** For different styles/colors of the buttons */
style?: ButtonStyles | TextStyles
/** text that appears on the button (max 80 characters) */
label?: string
/** the dev-define value of the option, max 100 characters for select or 4000 for input. */
value?: string
/** Emoji object that includes fields of name, id, and animated supporting unicode and custom emojis. */
emoji?: {
/** Emoji id */
id?: bigint
/** Emoji name */
name?: string
/** Whether this emoji is animated */
animated?: boolean
}
/** optional url for link-style buttons that can navigate a user to the web. Only type 5 Link buttons can have a url */
url?: string
/** List of channel types to include in a channel select menu options list */
channelTypes?: ChannelTypes[]
/** The choices! Maximum of 25 items. */
options?: SelectOption[]
/** A custom placeholder text if nothing is selected. Maximum 150 characters. */
placeholder?: string
/** The minimum number of items that must be selected. Default 1. Between 1-25. */
minValues?: number
/** The maximum number of items that can be selected. Default 1. Between 1-25. */
maxValues?: number
/** The minimum input length for a text input. Between 0-4000. */
minLength?: number
/** The maximum input length for a text input. Between 1-4000. */
maxLength?: number
/** a list of child components */
components?: Component[]
}

View File

@@ -1,5 +1,5 @@
import type { DiscordEmbed, EmbedTypes } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { DiscordEmbed } from '@discordeno/types'
import type { Bot, Embed } from '../index.js'
export function transformEmbed(bot: Bot, payload: DiscordEmbed): Embed {
const embed = {
@@ -54,50 +54,3 @@ export function transformEmbed(bot: Bot, payload: DiscordEmbed): Embed {
return bot.transformers.customizers.embed(bot, payload, embed)
}
export interface Embed {
description?: string
type?: EmbedTypes
url?: string
image?: {
proxyUrl?: string
height?: number
width?: number
url: string
}
video?: {
url?: string
proxyUrl?: string
height?: number
width?: number
}
title?: string
timestamp?: number
color?: number
footer?: {
iconUrl?: string
proxyIconUrl?: string
text: string
}
thumbnail?: {
proxyUrl?: string
height?: number
width?: number
url: string
}
provider?: {
name?: string
url?: string
}
author?: {
url?: string
iconUrl?: string
proxyIconUrl?: string
name: string
}
fields?: Array<{
inline?: boolean
name: string
value: string
}>
}

View File

@@ -1,5 +1,5 @@
import type { DiscordDefaultReactionEmoji, DiscordEmoji } from '@discordeno/types'
import type { Bot, User } from '../index.js'
import type { Bot, DefaultReactionEmoji, Emoji } from '../index.js'
import { EmojiToggles } from './toggles/emoji.js'
export function transformEmoji(bot: Bot, payload: DiscordEmoji): Emoji {
@@ -25,30 +25,3 @@ export function transformDefaultReactionEmoji(bot: Bot, payload: DiscordDefaultR
return bot.transformers.customizers.defaultReactionEmoji(bot, payload, defaultReactionEmoji)
}
export interface Emoji {
/** Emoji name (can only be null in reaction emoji objects) */
name?: string
/** Emoji id */
id?: bigint
/** Roles allowed to use this emoji */
roles?: bigint[]
/** User that created this emoji */
user?: User
/** Whether this emoji must be wrapped in colons */
requireColons?: boolean
/** Whether this emoji is managed */
managed?: boolean
/** Whether this emoji is animated */
animated?: boolean
/** Whether this emoji can be used, may be false due to loss of Server Boosts */
available?: boolean
toggles: EmojiToggles
}
export interface DefaultReactionEmoji {
/** The id of a guild's custom emoji */
emojiId: bigint
/** The unicode character of the emoji */
emojiName?: string
}

View File

@@ -1,5 +1,5 @@
import type { DiscordEntitlement, DiscordEntitlementType } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { DiscordEntitlement } from '@discordeno/types'
import type { Bot, Entitlement } from '../index.js'
export function transformEntitlement(bot: Bot, payload: DiscordEntitlement): Entitlement {
const props = bot.transformers.desiredProperties.entitlement
@@ -18,26 +18,3 @@ export function transformEntitlement(bot: Bot, payload: DiscordEntitlement): Ent
return bot.transformers.customizers.entitlement(bot, payload, entitlement)
}
export interface Entitlement {
/** ID of the entitlement */
id: bigint
/** ID of the SKU */
skuId: bigint
/** ID of the user that is granted access to the entitlement's sku */
userId?: bigint
/** ID of the guild that is granted access to the entitlement's sku */
guildId?: bigint
/** ID of the parent application */
applicationId: bigint
/** Type of entitlement */
type: DiscordEntitlementType
/** Entitlement was deleted */
deleted: boolean
/** Start date at which the entitlement is valid. Not present when using test entitlements */
startsAt?: number
/** Date at which the entitlement is no longer valid. Not present when using test entitlements */
endsAt?: number
/** For consumable items, whether or not the entitlement has been consumed */
consumed?: boolean
}

View File

@@ -1,5 +1,5 @@
import type { DiscordGetGatewayBot } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { Bot, GetGatewayBot } from '../index.js'
export function transformGatewayBot(bot: Bot, payload: DiscordGetGatewayBot): GetGatewayBot {
const gatewayBot = {
@@ -15,14 +15,3 @@ export function transformGatewayBot(bot: Bot, payload: DiscordGetGatewayBot): Ge
return bot.transformers.customizers.gatewayBot(bot, payload, gatewayBot)
}
export interface GetGatewayBot {
url: string
shards: number
sessionStartLimit: {
total: number
remaining: number
resetAfter: number
maxConcurrency: number
}
}

View File

@@ -1,18 +1,6 @@
import {
ChannelTypes,
type DefaultMessageNotificationLevels,
type DiscordGuild,
type DiscordPresenceUpdate,
type ExplicitContentFilterLevels,
type GuildNsfwLevel,
type MfaLevels,
type PremiumTiers,
type SystemChannelFlags,
type VerificationLevels,
} from '@discordeno/types'
import { ChannelTypes, type DiscordGuild, type DiscordPresenceUpdate } from '@discordeno/types'
import { Collection, iconHashToBigInt } from '@discordeno/utils'
import type { Bot, Channel, GuildFeatureKeys, Member, PresenceUpdate, Role, StageInstance, Sticker, VoiceState, WelcomeScreen } from '../index.js'
import type { Emoji } from '../transformers/emoji.js'
import type { Bot, Channel, Guild } from '../index.js'
import { GuildToggles } from './toggles/guild.js'
const baseGuild = {
@@ -150,112 +138,3 @@ export function transformGuild(bot: Bot, payload: { guild: DiscordGuild } & { sh
return bot.transformers.customizers.guild(bot, payload.guild, guild)
}
export interface Guild {
/** Guild name (2-100 characters, excluding trailing and leading whitespace) */
name: string
/** True if the user is the owner of the guild */
owner: boolean | undefined
/** Afk timeout in seconds */
afkTimeout: number
/** True if the server widget is enabled */
widgetEnabled?: boolean
/** Verification level required for the guild */
verificationLevel: VerificationLevels
/** Default message notifications level */
defaultMessageNotifications: DefaultMessageNotificationLevels
/** Explicit content filter level */
explicitContentFilter: ExplicitContentFilterLevels
/** Enabled guild features */
features: GuildFeatureKeys[]
/** Required MFA level for the guild */
mfaLevel: MfaLevels
/** System channel flags */
systemChannelFlags: SystemChannelFlags
/** True if this is considered a large guild */
large?: boolean
/** True if this guild is unavailable due to an outage */
unavailable?: boolean
/** Total number of members in this guild */
memberCount: number
/** The maximum number of presences for the guild (the default value, currently 25000, is in effect when null is returned) */
maxPresences?: number
/** The maximum number of members for the guild */
maxMembers?: number
/** The vanity url code for the guild */
vanityUrlCode?: string
/** The description of a guild */
description?: string
toggles: GuildToggles
shardId: number
/** Premium tier (Server Boost level) */
premiumTier: PremiumTiers
/** The number of boosts this guild currently has */
premiumSubscriptionCount?: number
/** The maximum amount of users in a video channel */
maxVideoChannelUsers?: number
/** Maximum amount of users in a stage video channel */
maxStageVideoChannelUsers?: number
/** Approximate number of members in this guild, returned from the GET /guilds/id endpoint when with_counts is true */
approximateMemberCount?: number
/** Approximate number of non-offline members in this guild, returned from the GET /guilds/id endpoint when with_counts is true */
approximatePresenceCount?: number
/** Guild NSFW level */
nsfwLevel: GuildNsfwLevel
/** Whether the guild has the boost progress bar enabled */
premiumProgressBarEnabled: boolean
/** Guild id */
id: bigint
/** Icon hash */
icon?: bigint
/** Icon hash, returned when in the template object */
iconHash?: bigint
/** Splash hash */
splash?: bigint
/** Discovery splash hash; only present for guilds with the "DISCOVERABLE" feature */
discoverySplash?: bigint
/** Id of the owner */
ownerId: bigint
/** Total permissions for the user in the guild (excludes overwrites and implicit permissions) */
permissions: bigint
/** Id of afk channel */
afkChannelId?: bigint
/** The channel id that the widget will generate an invite to, or null if set to no invite */
widgetChannelId?: bigint
/** Roles in the guild */
roles: Collection<bigint, Role>
/** Custom guild emojis */
emojis: Collection<bigint, Emoji>
/** Application id of the guild creator if it is bot-created */
applicationId?: bigint
/** The id of the channel where guild notices such as welcome messages and boost events are posted */
systemChannelId?: bigint
/** The id of the channel where community guilds can display rules and/or guidelines */
rulesChannelId?: bigint
/** When this guild was joined at */
joinedAt?: number
/** States of members currently in voice channels; lacks the guild_id key */
voiceStates: Collection<bigint, VoiceState>
/** Users in the guild */
members: Collection<bigint, Member>
/** Channels in the guild */
channels: Collection<bigint, Channel>
/** All active threads in the guild that the current user has permission to view */
threads: Collection<bigint, Channel>
/** Presences of the members in the guild, will only include non-offline members if the size is greater than large threshold */
presences?: PresenceUpdate[]
/** Banner hash */
banner?: bigint
/** The preferred locale of a Community guild; used in server discovery and notices from Discord; defaults to "en-US" */
preferredLocale: string
/** The id of the channel where admins and moderators of Community guilds receive notices from Discord */
publicUpdatesChannelId?: bigint
/** The welcome screen of a Community guild, shown to new members, returned in an Invite's guild object */
welcomeScreen?: WelcomeScreen
/** Stage instances in the guild */
stageInstances?: StageInstance[]
/** Custom guild stickers */
stickers?: Collection<bigint, Sticker>
/** The id of the channel where admins and moderators of Community guilds receive safety alerts from Discord */
safetyAlertsChannelId?: bigint
}

View File

@@ -35,6 +35,7 @@ export * from './team.js'
export * from './template.js'
export * from './threadMember.js'
export * from './toggles/index.js'
export * from './types.js'
export * from './user.js'
export * from './voiceRegion.js'
export * from './voiceState.js'

View File

@@ -1,5 +1,5 @@
import type { DiscordIntegrationCreateUpdate, IntegrationExpireBehaviors, OAuth2Scope } from '@discordeno/types'
import { type Bot, type User, iconHashToBigInt } from '../index.js'
import type { DiscordIntegrationCreateUpdate } from '@discordeno/types'
import { type Bot, type Integration, iconHashToBigInt } from '../index.js'
export function transformIntegration(bot: Bot, payload: DiscordIntegrationCreateUpdate): Integration {
const integration = {
@@ -35,32 +35,3 @@ export function transformIntegration(bot: Bot, payload: DiscordIntegrationCreate
return bot.transformers.customizers.integration(bot, payload, integration)
}
export interface Integration {
user?: User
enabled?: boolean
syncing?: boolean
roleId?: bigint
enableEmoticons?: boolean
expireBehavior?: IntegrationExpireBehaviors
expireGracePeriod?: number
syncedAt?: number
subscriberCount?: number
revoked?: boolean
application?: {
bot?: User
icon?: bigint
id: bigint
name: string
description: string
}
id: bigint
name: string
guildId: bigint
type: 'twitch' | 'youtube' | 'discord'
account: {
id: bigint
name: string
}
scopes: OAuth2Scope[]
}

View File

@@ -1,142 +1,25 @@
import {
type ApplicationCommandOptionTypes,
type ApplicationCommandTypes,
type BigString,
type ChannelTypes,
DiscordApplicationIntegrationType,
type DiscordInteraction,
type DiscordInteractionDataOption,
type InteractionCallbackData,
InteractionResponseTypes,
InteractionTypes,
type MessageComponentTypes,
MessageFlags,
} from '@discordeno/types'
import { Collection } from '@discordeno/utils'
import {
type Bot,
type Channel,
type Component,
DiscordApplicationIntegrationType,
type DiscordChannel,
type DiscordInteractionContextType,
type Guild,
type Interaction,
type InteractionDataOption,
type InteractionDataResolved,
type Member,
type Message,
} from '../index.js'
import type { DiscordInteractionDataResolved } from '../typings.js'
import type { Attachment } from './attachment.js'
import type { Member } from './member.js'
import type { Message } from './message.js'
import type { Role } from './role.js'
import type { User } from './user.js'
export interface Interaction extends BaseInteraction {
/** The bot object */
bot: Bot
/** Whether or not this interaction has been responded to. */
acknowledged: boolean
/** Id of the interaction */
id: bigint
/** Id of the application this interaction is for */
applicationId: bigint
/** The type of interaction */
type: InteractionTypes
/** Guild that the interaction was sent from */
guild: Guild
/** The guild it was sent from */
guildId?: bigint
/** The channel it was sent from */
channel: Partial<Channel>
/**
* The ID of channel it was sent from
*
* @remarks
* It is recommended that you begin using this channel field to identify the source channel of the interaction as they may deprecate the existing channel_id field in the future.
*/
channelId?: bigint
/** Guild member data for the invoking user, including permissions */
member?: Member
/** User object for the invoking user, if invoked in a DM */
user: User
/** A continuation token for responding to the interaction */
token: string
/** Read-only property, always `1` */
version: 1
/** For the message the button was attached to */
message?: Message
/** the command data payload */
data?: {
type?: ApplicationCommandTypes
componentType?: MessageComponentTypes
customId?: string
components?: Component[]
values?: string[]
name: string
resolved?: InteractionDataResolved
options?: InteractionDataOption[]
id?: bigint
targetId?: bigint
// guildId?: bigint
}
/** The selected language of the invoking user */
locale?: string
/** The guild's preferred locale, if invoked in a guild */
guildLocale?: string
/** The computed permissions for a bot or app in the context of a specific interaction (including channel overwrites) */
appPermissions: bigint
/** Mapping of installation contexts that the interaction was authorized for to related user or guild IDs. */
authorizingIntegrationOwners: Partial<Record<DiscordApplicationIntegrationType, bigint>>
/** Context where the interaction was triggered from */
context?: DiscordInteractionContextType
}
export interface BaseInteraction {
/**
* Sends a response to an interaction.
*
* @remarks
* This will send a {@link InteractionResponseTypes.ChannelMessageWithSource}, {@link InteractionResponseTypes.ApplicationCommandAutocompleteResult} or {@link InteractionResponseTypes.Modal} response based on the type of the interaction you are responding to.
*
* If the interaction has been already acknowledged, indicated by {@link Interaction.acknowledged}, it will send a followup message instead.
*
* Uses `interaction.type`, `interaction.token` and `interaction.id`, missing one of these in the desired proprieties may cause unexpected behavior.
*/
respond: (response: string | InteractionCallbackData, options?: { isPrivate?: boolean }) => Promise<Message | void>
/**
* Edit the original response of an interaction or a followup if the message id is provided.
*
* @remarks
* This will edit the original interaction response or, if the interaction has not yet been acknowledged and the type of the interaction is {@link InteractionTypes.MessageComponent} it will instead send a {@link InteractionResponseTypes.UpdateMessage} response instead.
*
* Uses `interaction.type`, `interaction.token` and `interaction.id`, missing one of these in the desired proprieties may cause unexpected behavior.
*/
edit: (response: string | InteractionCallbackData, messageId?: BigString) => Promise<Message | void>
/**
* Defer the interaction for updating the referenced message at a later time with {@link edit}.
*
* @remarks
* This will send a {@link InteractionResponseTypes.DeferredUpdateMessage} response.
*
* Uses `interaction.type`, `interaction.token` and `interaction.id`, missing one of these in the desired proprieties may cause unexpected behavior.
*/
deferEdit: () => Promise<void>
/**
* Defer the interaction for updating the response at a later time with {@link edit}.
*
* @remarks
* This will send a {@link InteractionResponseTypes.DeferredChannelMessageWithSource} response.
*
* Uses `interaction.type`, `interaction.token` and `interaction.id`, missing one of these in the desired proprieties may cause unexpected behavior.
*/
defer: (isPrivate?: boolean) => Promise<void>
/**
* Delete the original interaction response or a followup if the message id is provided.
*
* @remarks
* Uses `interaction.type` and `interaction.token`, missing one of these in the desired proprieties may cause unexpected behavior.
*/
delete: (messageId?: BigString) => Promise<void>
}
const baseInteraction: Partial<Interaction> & BaseInteraction = {
const baseInteraction = {
async respond(response, options) {
let type = InteractionResponseTypes.ChannelMessageWithSource
@@ -149,14 +32,14 @@ const baseInteraction: Partial<Interaction> & BaseInteraction = {
if (type === InteractionResponseTypes.ChannelMessageWithSource && options?.isPrivate) response.flags = MessageFlags.Ephemeral
// Since this has already been given a response, any further responses must be followups.
if (this.acknowledged) return await this.bot!.helpers.sendFollowupMessage(this.token!, response)
if (this.acknowledged) return await this.bot.helpers.sendFollowupMessage(this.token, response)
// Modals cannot be chained
if (this.type === InteractionTypes.ModalSubmit && type === InteractionResponseTypes.Modal)
throw new Error('Cannot respond to a modal interaction with another modal.')
this.acknowledged = true
return await this.bot!.helpers.sendInteractionResponse(this.id!, this.token!, { type, data: response })
return await this.bot.helpers.sendInteractionResponse(this.id, this.token, { type, data: response })
},
async edit(response, messageId) {
if (this.type === InteractionTypes.ApplicationCommandAutocomplete) throw new Error('Cannot edit an autocomplete interaction.')
@@ -165,7 +48,7 @@ const baseInteraction: Partial<Interaction> & BaseInteraction = {
if (typeof response === 'string') response = { content: response }
if (messageId) {
return await this.bot?.helpers.editFollowupMessage(this.token!, messageId, response)
return await this.bot?.helpers.editFollowupMessage(this.token, messageId, response)
}
if (!this.acknowledged) {
@@ -173,10 +56,10 @@ const baseInteraction: Partial<Interaction> & BaseInteraction = {
throw new Error("This interaction has not been responded to yet and this isn't a MessageComponent interaction.")
this.acknowledged = true
return await this.bot!.helpers.sendInteractionResponse(this.id!, this.token!, { type: InteractionResponseTypes.UpdateMessage, data: response })
return await this.bot.helpers.sendInteractionResponse(this.id, this.token, { type: InteractionResponseTypes.UpdateMessage, data: response })
}
return await this.bot!.helpers.editOriginalInteractionResponse(this.token!, response)
return await this.bot.helpers.editOriginalInteractionResponse(this.token, response)
},
async deferEdit() {
if (this.type === InteractionTypes.ApplicationCommandAutocomplete) throw new Error('Cannot edit an autocomplete interaction.')
@@ -186,13 +69,13 @@ const baseInteraction: Partial<Interaction> & BaseInteraction = {
throw new Error("Cannot defer to then edit an interaction that isn't a MessageComponent interaction.")
this.acknowledged = true
return await this.bot!.helpers.sendInteractionResponse(this.id!, this.token!, { type: InteractionResponseTypes.DeferredUpdateMessage })
return await this.bot.helpers.sendInteractionResponse(this.id, this.token, { type: InteractionResponseTypes.DeferredUpdateMessage })
},
async defer(isPrivate) {
if (this.acknowledged) throw new Error('Cannot defer an already responded interaction.')
this.acknowledged = true
return await this.bot!.helpers.sendInteractionResponse(this.id!, this.token!, {
return await this.bot.helpers.sendInteractionResponse(this.id, this.token, {
type: InteractionResponseTypes.DeferredChannelMessageWithSource,
data: {
flags: isPrivate ? MessageFlags.Ephemeral : undefined,
@@ -202,10 +85,10 @@ const baseInteraction: Partial<Interaction> & BaseInteraction = {
async delete(messageId) {
if (this.type === InteractionTypes.ApplicationCommandAutocomplete) throw new Error('Cannot delete an autocomplete interaction')
if (messageId) return await this.bot?.helpers.deleteFollowupMessage(this.token!, messageId)
else return await this.bot?.helpers.deleteOriginalInteractionResponse(this.token!)
if (messageId) return await this.bot?.helpers.deleteFollowupMessage(this.token, messageId)
else return await this.bot?.helpers.deleteOriginalInteractionResponse(this.token)
},
}
} as Interaction
export function transformInteraction(bot: Bot, payload: { interaction: DiscordInteraction; shardId: number }): Interaction {
const guildId = payload.interaction.guild_id ? bot.transformers.snowflake(payload.interaction.guild_id) : undefined
@@ -354,20 +237,3 @@ export function transformInteractionDataResolved(bot: Bot, resolved: DiscordInte
return transformed
}
export interface InteractionDataResolved {
messages?: Collection<bigint, Message>
users?: Collection<bigint, User>
members?: Collection<bigint, Member>
roles?: Collection<bigint, Role>
channels?: Collection<bigint, { id: bigint; name: string; type: ChannelTypes; permissions: bigint }>
attachments?: Collection<bigint, Attachment>
}
export interface InteractionDataOption {
name: string
type: ApplicationCommandOptionTypes
value?: string | number | boolean
options?: InteractionDataOption[]
focused?: boolean
}

View File

@@ -1,6 +1,5 @@
import type { DiscordApplication, DiscordInviteCreate, DiscordInviteMetadata, DiscordInviteType } from '@discordeno/types'
import { type Application, type Bot, type ScheduledEvent, type User, isInviteWithMetadata } from '../index.js'
import type { InviteStageInstance } from './stageInviteInstance.js'
import type { DiscordApplication, DiscordInviteCreate, DiscordInviteMetadata } from '@discordeno/types'
import { type Bot, type Invite, isInviteWithMetadata } from '../index.js'
export function transformInvite(bot: Bot, payload: { invite: DiscordInviteCreate | DiscordInviteMetadata; shardId: number }): Invite {
const props = bot.transformers.desiredProperties.invite
@@ -47,42 +46,3 @@ export function transformInvite(bot: Bot, payload: { invite: DiscordInviteCreate
return bot.transformers.customizers.invite(bot, payload.invite, invite)
}
export interface Invite {
/** The type of invite */
type: DiscordInviteType
/** The channel the invite is for */
channelId: bigint
/** The unique invite code */
code: string
/** The time at which the invite was created */
createdAt: number
/** The guild of the invite */
guildId?: bigint
/** The user that created the invite */
inviter?: User
/** How long the invite is valid for (in seconds) */
maxAge: number
/** The maximum number of times the invite can be used */
maxUses: number
/** The type of target for this voice channel invite */
targetType: number
/** The target user for this invite */
targetUser: User
/** The embedded application to open for this voice channel embedded application invite */
targetApplication?: Application
/** Whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role) */
temporary: boolean
/** How many times the invite has been used (always will be 0) */
uses: number
/** Approximate count of online members (only present when target_user is set) */
approximateMemberCount: number
/** Stage instance data if there is a public Stage instance in the Stage channel this invite is for */
stageInstance?: InviteStageInstance
/** The expiration date of this invite, returned from the GET /invites/code endpoint when with_expiration is true */
expiresAt?: number
/** guild scheduled event data */
guildScheduledEvent?: ScheduledEvent
/** Approximate count of online members (only present when target_user is set) */
approximatePresenceCount?: number
}

View File

@@ -1,12 +1,11 @@
import { type BigString, type DiscordMember } from '@discordeno/types'
import { iconHashToBigInt } from '@discordeno/utils'
import type { Bot } from '../bot.js'
import type { AvatarDecorationData } from './avatarDecorationData.js'
import { Permissions } from './toggles/Permissions.js'
import { MemberToggles } from './toggles/member.js'
import type { User } from './user.js'
import type { Member } from './types.js'
const baseMember: Partial<Member> & BaseMember = {
const baseMember = {
get deaf() {
return !!this.toggles?.has('deaf')
},
@@ -31,7 +30,7 @@ const baseMember: Partial<Member> & BaseMember = {
get completedOnboarding() {
return !!this.toggles?.completedOnboarding
},
}
} as Member
export function transformMember(bot: Bot, payload: DiscordMember, guildId: BigString, userId: BigString): Member {
const member: Member = Object.create(baseMember)
@@ -48,57 +47,9 @@ export function transformMember(bot: Bot, payload: DiscordMember, guildId: BigSt
member.communicationDisabledUntil = Date.parse(payload.communication_disabled_until)
if (props.avatar && payload.avatar) member.avatar = iconHashToBigInt(payload.avatar)
if (props.permissions && payload.permissions) member.permissions = new Permissions(payload.permissions)
if (props.deaf || props.mute || props.pending || props.flags) {
member.toggles = new MemberToggles(payload)
}
if (props.toggles) member.toggles = new MemberToggles(payload)
if (props.avatarDecorationData && payload.avatar_decoration_data)
member.avatarDecorationData = bot.transformers.avatarDecorationData(bot, payload.avatar_decoration_data)
return bot.transformers.customizers.member(bot, payload, member)
}
export interface BaseMember {
/** Whether the user is deafened in voice channels */
deaf?: boolean
/** Whether the user is muted in voice channels */
mute?: boolean
/** Whether the user has not yet passed the guild's Membership Screening requirements */
pending?: boolean
/** Member has left and rejoined the guild */
didRejoin?: boolean
/** Member has completed onboarding */
startedOnboarding?: boolean
/** Member is exempt from guild verification requirements */
bypassesVerification?: boolean
/** Member has started onboarding */
completedOnboarding?: boolean
/** Guild member flags */
flags: number
}
export interface Member extends BaseMember {
/** The user id of the member. */
id: bigint
/** The compressed form of all the boolean values on this user. */
toggles?: MemberToggles
/** The guild id where this member is. */
guildId: bigint
/** The user this guild member represents */
user?: User
/** This users guild nickname */
nick?: string
/** The members custom avatar for this server. */
avatar?: bigint
/** Array of role object ids */
roles: bigint[]
/** When the user joined the guild */
joinedAt: number
/** When the user started boosting the guild */
premiumSince?: number
/** The permissions this member has in the guild. Only present on interaction events. */
permissions?: Permissions
/** 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 */
communicationDisabledUntil?: number
/** data for the member's guild avatar decoration */
avatarDecorationData: AvatarDecorationData
}

View File

@@ -3,24 +3,15 @@ import {
type DiscordMessage,
type DiscordMessageCall,
type DiscordMessageInteractionMetadata,
type InteractionTypes,
type MessageActivityTypes,
MessageFlags,
type MessageTypes,
type StickerFormatTypes,
} from '@discordeno/types'
import { CHANNEL_MENTION_REGEX } from '../constants.js'
import { type Bot, type Poll, snowflakeToTimestamp } from '../index.js'
import type { Attachment } from './attachment.js'
import type { Channel } from './channel.js'
import type { Component } from './component.js'
import type { Embed } from './embed.js'
import type { Emoji } from './emoji.js'
import type { Member } from './member.js'
import { type Bot, type Message, type MessageCall, type MessageInteractionMetadata, snowflakeToTimestamp } from '../index.js'
import { ToggleBitfield } from './toggles/ToggleBitfield.js'
import type { User } from './user.js'
const baseMessage: Partial<Message> & MessageBase = {
const EMPTY_STRING = ''
const baseMessage = {
get crossposted() {
return this.flags?.contains(MessageFlags.Crossposted) ?? false
},
@@ -131,153 +122,7 @@ const baseMessage: Partial<Message> & MessageBase = {
if (value) this.flags.add(MessageFlags.Urgent)
else this.flags.remove(MessageFlags.Urgent)
},
}
export interface MessageBase {
/** Holds all the boolean values on this message. */
bitfield?: ToggleBitfield
/** Whether this message has been published to subscribed channels (via Channel Following) */
crossposted: boolean
/** Whether this message is only visible to the user who invoked the Interaction */
ephemeral: boolean
/** Whether this message failed to mention some roles and add their members to the thread */
failedToMentionSomeRolesInThread: boolean
/** Message flags combined as a bitfield */
flags?: ToggleBitfield
/** Whether this message has an associated thread, with the same id as the message */
hasThread: boolean
/** Whether this message originated from a message in another channel (via Channel Following) */
isCrosspost: boolean
/** Whether this message is an Interaction Response and the bot is "thinking" */
loading: boolean
/** The ids of the users who were mentioned in this message. */
mentionedUserIds: bigint[]
/** Whether this message mentions everyone */
mentionEveryone: boolean
/** Whether this message is pinned */
pinned: boolean
/** Whether the source message for this crosspost has been deleted (via Channel Following) */
sourceMessageDeleted: boolean
/** Whether do not include any embeds when serializing this message */
suppressEmbeds: boolean
/** Whether this message will not trigger push and desktop notifications */
suppressNotifications: boolean
/** The timestamp in milliseconds when this message was created */
timestamp: number
/** Whether this was a TTS message. */
tts: boolean
/** Whether this message came from the urgent message system */
urgent: boolean
}
export interface Message extends MessageBase {
/** Sent with Rich Presence-related chat embeds */
activity?: {
/** Type of message activity */
type: MessageActivityTypes
/** party_id from a Rich Presence event */
partyId?: string
}
/** if the message is an Interaction or application-owned webhook, this is the id of the application */
applicationId?: bigint
/** Any attached files on this message. */
attachments?: Attachment[]
/** The author of this message (not guaranteed to be a valid user) Note: The author object follows the structure of the user object, but is only a valid user in the case where the message is generated by a user or bot user. If the message is generated by a webhook, the author object corresponds to the webhook's id, username, and avatar. You can tell if a message is generated by a webhook by checking for the webhook_id on the message object. */
author: User
/** id of the channel the message was sent in */
channelId: bigint
/** The components related to this message */
components: Component[]
/** Contents of the message */
content: string
/** The timestamp in milliseconds when this message was edited last. */
editedTimestamp?: number
/** Any embedded content */
embeds?: Embed[]
/** id of the guild the message was sent in Note: For MESSAGE_CREATE and MESSAGE_UPDATE events, the message object may not contain a guild_id or member field since the events are sent directly to the receiving user and the bot who sent the message, rather than being sent through the guild like non-ephemeral messages. */
guildId?: bigint
/** id of the message */
id: bigint
/** sent if the message is sent as a result of an interaction */
interactionMetadata?: MessageInteractionMetadata
/**
* Sent if the message is a response to an Interaction
*
* @deprecated Deprecated in favor of {@link interactionMetadata}
*/
interaction?: {
/** Id of the interaction */
id: bigint
/** The member who invoked the interaction in the guild */
member?: Member
/** The name of the ApplicationCommand including the name of the subcommand/subcommand group */
name: string
/** The type of interaction */
type: InteractionTypes
/** The user who invoked the interaction */
user: User
}
/** Member properties for this message's author Note: The member object exists in MESSAGE_CREATE and MESSAGE_UPDATE events from text-based guild channels. This allows bots to obtain real-time member data without requiring bots to store member state in memory. */
member?: Member
/** Users specifically mentioned in the message Note: The user objects in the mentions array will only have the partial member field present in MESSAGE_CREATE and MESSAGE_UPDATE events from text-based guild channels. */
mentions?: User[]
/** Channels specifically mentioned in this message Note: Not all channel mentions in a message will appear in mention_channels. Only textual channels that are visible to everyone in a discoverable guild will ever be included. Only crossposted messages (via Channel Following) currently include mention_channels at all. If no mentions in the message meet these requirements, this field will not be sent. */
mentionedChannelIds?: bigint[]
/** Roles specifically mentioned in this message */
mentionedRoleIds?: bigint[]
/** Data showing the source of a crossposted channel follow add, pin or reply message */
messageReference?: {
/** id of the originating message's channel Note: channel_id is optional when creating a reply, but will always be present when receiving an event/response that includes this data model. */
channelId?: bigint
/** id of the originating message's guild */
guildId?: bigint
/** id of the originating message */
messageId?: bigint
}
/** Used for validating a message was sent */
nonce?: string | number
/** Reactions on this message. */
reactions?: Array<{
/** Whether the current user reacted using this emoji */
me: boolean
/** Whether the current user super-reacted using this emoji */
meBurst: boolean
/** Times this emoji has been used to react */
count: number
/** Reaction count details object */
countDetails: {
/** Count of super reactions */
burst: number
/** Count of normal reactions */
normal: number
}
/** Emoji information */
emoji: Emoji
/** HEX colors used for super reaction */
burstColors: string[]
}>
/** Sent if the message contains stickers */
stickerItems?: Array<{
/** The id of this sticker. */
id: bigint
/** The name of this sticker. */
name: string
/** The type of this stickers format. */
formatType: StickerFormatTypes
}>
/** Type of message */
type: MessageTypes
/** The thread that was started from this message, includes thread member object */
thread?: Channel
/** If the message is generated by a webhook, this is the webhook's id */
webhookId?: bigint
/** A poll! */
poll?: Poll
/** The call associated with the message */
call?: MessageCall
}
const EMPTY_STRING = ''
} as Message
export function transformMessage(bot: Bot, payload: DiscordMessage): Message {
const guildId = payload.guild_id ? bot.transformers.snowflake(payload.guild_id) : undefined
@@ -300,33 +145,30 @@ export function transformMessage(bot: Bot, payload: DiscordMessage): Message {
if (props.embeds && payload.embeds?.length) message.embeds = payload.embeds.map((embed) => bot.transformers.embed(bot, embed))
if (props.guildId && guildId) message.guildId = guildId
if (props.id && payload.id) message.id = bot.transformers.snowflake(payload.id)
if (payload.interaction_metadata) message.interactionMetadata = transformMessageInteractionMetadata(bot, payload.interaction_metadata)
if (payload.interaction) {
if (props.interactionMetadata && payload.interaction_metadata)
message.interactionMetadata = transformMessageInteractionMetadata(bot, payload.interaction_metadata)
if (props.interaction && payload.interaction) {
const interaction = {} as NonNullable<Message['interaction']>
let edited = false
if (props.interaction.id) {
const messageInteractionProps = bot.transformers.desiredProperties.messageInteraction
if (messageInteractionProps.id) {
interaction.id = bot.transformers.snowflake(payload.interaction.id)
edited = true
}
if (props.interaction.member && payload.interaction.member) {
if (messageInteractionProps.member && payload.interaction.member) {
// @ts-expect-error TODO: partial - check why this is partial and handle as needed
interaction.member = bot.transformers.member(bot, payload.interaction.member, guildId, payload.interaction.user.id)
edited = true
}
if (props.interaction.name) {
if (messageInteractionProps.name) {
interaction.name = payload.interaction.name
edited = true
}
if (props.interaction.type) {
if (messageInteractionProps.type) {
interaction.type = payload.interaction.type
edited = true
}
if (props.interaction.user) {
if (messageInteractionProps.user) {
interaction.user = bot.transformers.user(bot, payload.interaction.user)
edited = true
}
if (edited) message.interaction = interaction
message.interaction = interaction
}
if (props.member && guildId && payload.member) message.member = bot.transformers.member(bot, payload.member, guildId, userId)
if (payload.mention_everyone) message.mentionEveryone = true
@@ -344,24 +186,21 @@ export function transformMessage(bot: Bot, payload: DiscordMessage): Message {
if (props.mentionedRoleIds && payload.mention_roles?.length)
message.mentionedRoleIds = payload.mention_roles.map((id) => bot.transformers.snowflake(id))
if (props.mentions && payload.mentions?.length) message.mentions = payload.mentions.map((user) => bot.transformers.user(bot, user))
if (payload.message_reference) {
if (props.messageReference && payload.message_reference) {
const reference = {} as NonNullable<Message['messageReference']>
let edited = false
const messageReferenceProps = bot.transformers.desiredProperties.messageReference
if (props.messageReference.channelId && payload.message_reference.channel_id) {
if (messageReferenceProps.channelId && payload.message_reference.channel_id) {
reference.channelId = bot.transformers.snowflake(payload.message_reference.channel_id)
edited = true
}
if (props.messageReference.guildId && payload.message_reference.guild_id) {
if (messageReferenceProps.guildId && payload.message_reference.guild_id) {
reference.guildId = bot.transformers.snowflake(payload.message_reference.guild_id)
edited = true
}
if (props.messageReference.messageId && payload.message_reference.message_id) {
if (messageReferenceProps.messageId && payload.message_reference.message_id) {
reference.messageId = bot.transformers.snowflake(payload.message_reference.message_id)
edited = true
}
if (edited) message.messageReference = reference
message.messageReference = reference
}
if (props.nonce && payload.nonce) message.nonce = payload.nonce
if (payload.pinned) message.pinned = true
@@ -395,7 +234,7 @@ export function transformMessage(bot: Bot, payload: DiscordMessage): Message {
}
export function transformMessageInteractionMetadata(bot: Bot, payload: DiscordMessageInteractionMetadata): MessageInteractionMetadata {
const props = bot.transformers.desiredProperties.message.interactionMetadata
const props = bot.transformers.desiredProperties.messageInteractionMetadata
const metadata = {} as MessageInteractionMetadata
if (props.id) metadata.id = bot.transformers.snowflake(payload.id)
@@ -424,34 +263,10 @@ export function transformMessageInteractionMetadata(bot: Bot, payload: DiscordMe
export function transformMessageCall(bot: Bot, payload: DiscordMessageCall): MessageCall {
const call = {} as MessageCall
const props = bot.transformers.desiredProperties.message.call
const props = bot.transformers.desiredProperties.messageCall
if (props.participants && payload.participants) call.participants = payload.participants.map((x) => bot.transformers.snowflake(x))
if (props.endedTimestamp && payload.ended_timestamp) call.endedTimestamp = Date.parse(payload.ended_timestamp)
return bot.transformers.customizers.messageCall(bot, payload, call)
}
export interface MessageInteractionMetadata {
/** Id of the interaction */
id: bigint
/** The type of interaction */
type: InteractionTypes
/** User who triggered the interaction */
user: User
/** IDs for installation context(s) related to an interaction */
authorizingIntegrationOwners: Partial<Record<DiscordApplicationIntegrationType, bigint>>
/** ID of the original response message, present only on follow-up messages */
originalResponseMessageId?: bigint
/** ID of the message that contained interactive component, present only on messages created from component interactions */
interactedMessageId?: bigint
/** Metadata for the interaction that was used to open the modal, present only on modal submit interactions */
triggeringInteractionMetadata?: MessageInteractionMetadata
}
export interface MessageCall {
/** Array of user object ids that participated in the call */
participants: bigint[]
/** Time when call ended */
endedTimestamp: number
}

View File

@@ -1,11 +1,5 @@
import type {
DiscordGuildOnboarding,
DiscordGuildOnboardingMode,
DiscordGuildOnboardingPrompt,
DiscordGuildOnboardingPromptOption,
DiscordGuildOnboardingPromptType,
} from '@discordeno/types'
import { type Bot, type Emoji } from '../index.js'
import type { DiscordGuildOnboarding, DiscordGuildOnboardingPrompt, DiscordGuildOnboardingPromptOption } from '@discordeno/types'
import { type Bot, type GuildOnboarding, type GuildOnboardingPrompt, type GuildOnboardingPromptOption } from '../index.js'
export function transformGuildOnboarding(bot: Bot, payload: DiscordGuildOnboarding): GuildOnboarding {
const props = bot.transformers.desiredProperties.guildOnboarding
@@ -22,7 +16,7 @@ export function transformGuildOnboarding(bot: Bot, payload: DiscordGuildOnboardi
}
export function transformGuildOnboardingPrompt(bot: Bot, payload: DiscordGuildOnboardingPrompt): GuildOnboardingPrompt {
const props = bot.transformers.desiredProperties.guildOnboarding.prompts
const props = bot.transformers.desiredProperties.guildOnboardingPrompt
const prompt = {} as GuildOnboardingPrompt
if (props.id && payload.id) prompt.id = bot.transformers.snowflake(prompt.id)
@@ -37,7 +31,7 @@ export function transformGuildOnboardingPrompt(bot: Bot, payload: DiscordGuildOn
}
export function transformGuildOnboardingPromptOption(bot: Bot, payload: DiscordGuildOnboardingPromptOption): GuildOnboardingPromptOption {
const props = bot.transformers.desiredProperties.guildOnboarding.prompts.options
const props = bot.transformers.desiredProperties.guildOnboardingPromptOption
const option = {} as GuildOnboardingPromptOption
if (props.id && payload.id) option.id = bot.transformers.snowflake(payload.id)
@@ -49,48 +43,3 @@ export function transformGuildOnboardingPromptOption(bot: Bot, payload: DiscordG
return option
}
export interface GuildOnboarding {
/** ID of the guild this onboarding is part of */
guildId: bigint
/** Prompts shown during onboarding and in customize community */
prompts: GuildOnboardingPrompt[]
/** Channel IDs that members get opted into automatically */
defaultChannelIds: bigint[]
/** Whether onboarding is enabled in the guild */
enabled: boolean
/** Current mode of onboarding */
mode: DiscordGuildOnboardingMode
}
export interface GuildOnboardingPrompt {
/** ID of the prompt */
id: bigint
/** Type of prompt */
type: DiscordGuildOnboardingPromptType
/** Options available within the prompt */
options: GuildOnboardingPromptOption[]
/** Title of the prompt */
title: string
/** Indicates whether users are limited to selecting one option for the prompt */
singleSelect: boolean
/** Indicates whether the prompt is required before a user completes the onboarding flow */
required: boolean
/** Indicates whether the prompt is present in the onboarding flow. If `false`, the prompt will only appear in the Channels & Roles tab */
inOnboarding: boolean
}
export interface GuildOnboardingPromptOption {
/** ID of the prompt option */
id: bigint
/** IDs for channels a member is added to when the option is selected */
channelIds: bigint[]
/** IDs for roles assigned to a member when the option is selected */
roleIds: bigint[]
/** Emoji of the option */
emoji: Emoji
/** Title of the option */
title: string
/** Description of the option */
description: string | undefined
}

View File

@@ -1,5 +1,5 @@
import type { DiscordEmoji, DiscordPoll, DiscordPollLayoutType, DiscordPollMedia } from '@discordeno/types'
import type { Bot, Emoji } from '../index.js'
import type { DiscordEmoji, DiscordPoll, DiscordPollMedia } from '@discordeno/types'
import type { Bot, Poll, PollMedia, PollResult } from '../index.js'
export function transformPoll(bot: Bot, payload: DiscordPoll): Poll {
const props = bot.transformers.desiredProperties.poll
@@ -13,9 +13,10 @@ export function transformPoll(bot: Bot, payload: DiscordPoll): Poll {
if (props.layoutType) poll.layoutType = payload.layout_type
if (props.results && payload.results) {
poll.results = {} as PollResult
const pollResultProps = bot.transformers.desiredProperties.pollResult
if (props.results.isFinalized && payload.results.is_finalized) poll.results.isFinalized = payload.results.is_finalized
if (props.results.answerCounts && payload.results.answer_counts)
if (pollResultProps.isFinalized && payload.results.is_finalized) poll.results.isFinalized = payload.results.is_finalized
if (pollResultProps.answerCounts && payload.results.answer_counts)
poll.results.answerCounts = payload.results.answer_counts.map((x) => ({ id: x.id, count: x.count, meVoted: x.me_voted }))
}
@@ -31,75 +32,3 @@ export function transformPollMedia(bot: Bot, payload: DiscordPollMedia): PollMed
return bot.transformers.customizers.pollMedia(bot, payload, pollMedia)
}
export interface Poll {
/** The question of the poll. Only `text` is supported. */
question: PollMedia
/** Each of the answers available in the poll. There is a maximum of 10 answers per poll. */
answers: PollAnswer[]
/**
* The time when the poll ends.
*
* @remarks
* `expiry` is marked as nullable to support non-expiring polls in the future, but all polls have an expiry currently.
*/
expiry: number | null
/** Whether a user can select multiple answers */
allowMultiselect: boolean
/** The layout type of the poll */
layoutType: DiscordPollLayoutType
/**
* The results of the poll
*
* @remarks
* This value will not be sent by discord under specific conditions where they don't fetch them on their backend. When this value is missing it should be interpreted as "Unknown results" and not as "No results"
* The results may not be totally accurate while the poll has not ended. When it ends discord will re-calculate all the results and set {@link DiscordPollResult.is_finalized} to true
*/
results?: PollResult
}
export interface PollMedia {
/**
* The text of the field
*
* @remarks
* `text` should always be non-null for both questions and answers, but this is subject to changes.
* The maximum length of `text` is 300 for the question, and 55 for any answer.
*/
text?: string
/**
* The emoji of the field
*
* @remarks
* When creating a poll answer with an emoji, one only needs to send either the `id` (custom emoji) or `name` (default emoji) as the only field.
*/
emoji?: Partial<Emoji>
}
export interface PollAnswer {
/**
* The id of the answer
*
* @remarks
* This id labels each answer. It starts at 1 and goes up sequentially. Discord recommend against depending on this sequence as it is an implementation detail.
*/
answerId: number
/** The data of the answer */
pollMedia: PollMedia
}
export interface PollResult {
/** Whether the votes have been precisely counted */
isFinalized: boolean
/** The counts for each answer */
answerCounts: PollAnswerCount[]
}
export interface PollAnswerCount {
/** The {@link PollAnswer.answerId | answerId} */
id: number
/** The number of votes for this answer */
count: number
/** Whether the current user voted for this answer */
meVoted: boolean
}

View File

@@ -1,5 +1,5 @@
import { type DiscordPresenceUpdate, PresenceStatus } from '@discordeno/types'
import type { Activity, Bot, User } from '../index.js'
import type { Bot, PresenceUpdate } from '../index.js'
export function transformPresence(bot: Bot, payload: DiscordPresenceUpdate): PresenceUpdate {
const presence = {
@@ -14,13 +14,3 @@ export function transformPresence(bot: Bot, payload: DiscordPresenceUpdate): Pre
return bot.transformers.customizers.presence(bot, payload, presence)
}
export interface PresenceUpdate {
desktop?: string
mobile?: string
web?: string
user: User
guildId: bigint
status: PresenceStatus
activities: Activity[]
}

View File

@@ -1,5 +1,4 @@
import type { Bot, DiscordActivity } from '../../index.js'
import type { Activity } from '../activity.js'
import type { Activity, Bot, DiscordActivity } from '../../index.js'
export function transformActivityToDiscordActivity(_bot: Bot, payload: Activity): DiscordActivity {
return {

View File

@@ -1,5 +1,4 @@
import { type Bot, type DiscordApplication, iconBigintToHash } from '../../index.js'
import type { Application } from '../application.js'
import { type Application, type Bot, type DiscordApplication, iconBigintToHash } from '../../index.js'
export function transformApplicationToDiscordApplication(bot: Bot, payload: Application): DiscordApplication {
return {

View File

@@ -1,5 +1,4 @@
import type { Bot, DiscordApplicationCommand } from '../../index.js'
import type { ApplicationCommand } from '../applicationCommand.js'
import type { ApplicationCommand, Bot, DiscordApplicationCommand } from '../../index.js'
export function transformApplicationCommandToDiscordApplicationCommand(bot: Bot, payload: ApplicationCommand): DiscordApplicationCommand {
return {

View File

@@ -1,5 +1,4 @@
import type { Bot, DiscordApplicationCommandOption } from '../../index.js'
import type { ApplicationCommandOption } from '../applicationCommandOption.js'
import type { ApplicationCommandOption, Bot, DiscordApplicationCommandOption } from '../../index.js'
export function transformApplicationCommandOptionToDiscordApplicationCommandOption(
bot: Bot,

View File

@@ -1,5 +1,4 @@
import type { Bot, Camelize, DiscordApplicationCommandOptionChoice } from '../../index.js'
import type { ApplicationCommandOptionChoice } from '../applicationCommandOptionChoice.js'
import type { ApplicationCommandOptionChoice, Bot, Camelize, DiscordApplicationCommandOptionChoice } from '../../index.js'
export function transformApplicationCommandOptionChoiceToDiscordApplicationCommandOptionChoice(
_bot: Bot,

View File

@@ -1,9 +1,8 @@
import type { Bot, DiscordGuildApplicationCommandPermissions } from '../../index.js'
import type { ApplicationCommandPermission } from '../applicationCommandPermission.js'
import type { Bot, DiscordGuildApplicationCommandPermissions, GuildApplicationCommandPermissions } from '../../index.js'
export function transformApplicationCommandPermissionToDiscordApplicationCommandPermission(
bot: Bot,
payload: ApplicationCommandPermission,
payload: GuildApplicationCommandPermissions,
): DiscordGuildApplicationCommandPermissions {
return {
id: bot.transformers.reverse.snowflake(payload.id),

View File

@@ -1,5 +1,4 @@
import type { Bot, DiscordAttachment } from '../../index.js'
import type { Attachment } from '../attachment.js'
import type { Attachment, Bot, DiscordAttachment } from '../../index.js'
export function transformAttachmentToDiscordAttachment(bot: Bot, payload: Attachment): DiscordAttachment {
return {

View File

@@ -1,6 +1,5 @@
import type { DiscordAuditLogEntry } from '@discordeno/types'
import { type Bot, iconBigintToHash } from '../../index.js'
import type { AuditLogEntry } from '../auditLogEntry.js'
import { type AuditLogEntry, type Bot, iconBigintToHash } from '../../index.js'
export function transformAuditLogEntryToDiscordAuditLogEntry(bot: Bot, payload: AuditLogEntry): DiscordAuditLogEntry {
return {

View File

@@ -1,6 +1,5 @@
import type { Bot } from '../../index.js'
import type { Bot, Component } from '../../index.js'
import type { DiscordComponent } from '../../typings.js'
import type { Component } from '../component.js'
export function transformComponentToDiscordComponent(bot: Bot, payload: Component): DiscordComponent {
return {

View File

@@ -1,6 +1,5 @@
import type { DiscordEmbed } from '@discordeno/types'
import type { Bot } from '../../index.js'
import type { Embed } from '../embed.js'
import type { Bot, Embed } from '../../index.js'
export function transformEmbedToDiscordEmbed(_bot: Bot, payload: Embed): DiscordEmbed {
return {

View File

@@ -1,6 +1,5 @@
import type { DiscordEmoji } from '@discordeno/types'
import type { Bot } from '../../index.js'
import type { Emoji } from '../emoji.js'
import type { Bot, Emoji } from '../../index.js'
export function transformEmojiToDiscordEmoji(bot: Bot, payload: Emoji): DiscordEmoji {
return {

View File

@@ -1,5 +1,5 @@
import type { DiscordGetGatewayBot } from '@discordeno/types'
import type { GetGatewayBot } from '../gatewayBot.js'
import type { GetGatewayBot } from '../types.js'
export function transformGatewayBotToDiscordGatewayBot(payload: GetGatewayBot): DiscordGetGatewayBot {
return {

View File

@@ -1,8 +1,7 @@
import type { DiscordMember, DiscordUser } from '@discordeno/types'
import { iconBigintToHash } from '@discordeno/utils'
import type { Bot } from '../../bot.js'
import type { Member } from '../member.js'
import type { User } from '../user.js'
import type { Member, User } from '../types.js'
export function transformUserToDiscordUser(_bot: Bot, payload: User): DiscordUser {
return {

View File

@@ -1,6 +1,5 @@
import { type DiscordPresenceUpdate, PresenceStatus } from '@discordeno/types'
import type { Bot } from '../../index.js'
import type { PresenceUpdate } from '../presence.js'
import type { Bot, PresenceUpdate } from '../../index.js'
export const reverseStatusTypes = Object.freeze({
0: 'online',

View File

@@ -1,6 +1,5 @@
import type { DiscordTeam } from '@discordeno/types'
import { type Bot, iconBigintToHash } from '../../index.js'
import type { Team } from '../team.js'
import { type Bot, type Team, iconBigintToHash } from '../../index.js'
export function transformTeamToDiscordTeam(bot: Bot, payload: Team): DiscordTeam {
const id = payload.id.toString()

View File

@@ -1,6 +1,5 @@
import type { DiscordGuildWidgetSettings } from '@discordeno/types'
import type { Bot } from '../../index.js'
import type { GuildWidgetSettings } from '../widgetSettings.js'
import type { Bot, GuildWidgetSettings } from '../../index.js'
export function transformWidgetSettingsToDiscordWidgetSettings(_bot: Bot, payload: GuildWidgetSettings): DiscordGuildWidgetSettings {
return {

View File

@@ -1,9 +1,9 @@
import type { BigString, DiscordRole, RoleFlags } from '@discordeno/types'
import { type Bot, iconHashToBigInt } from '../index.js'
import type { BigString, DiscordRole } from '@discordeno/types'
import { type Bot, type Role, iconHashToBigInt } from '../index.js'
import { Permissions } from './toggles/Permissions.js'
import { RoleToggles } from './toggles/role.js'
const baseRole: Partial<Role> & BaseRole = {
const baseRole = {
get tags() {
return {
botId: this.internalTags?.botId,
@@ -38,7 +38,7 @@ const baseRole: Partial<Role> & BaseRole = {
get guildConnections() {
return !!this.toggles?.has('guildConnections')
},
}
} as Role
export function transformRole(bot: Bot, payload: { role: DiscordRole } & { guildId: BigString }): Role {
const role: Role = Object.create(baseRole)
@@ -52,88 +52,16 @@ export function transformRole(bot: Bot, payload: { role: DiscordRole } & { guild
if (props.icon && payload.role.icon) role.icon = iconHashToBigInt(payload.role.icon)
if (props.unicodeEmoji && payload.role.unicode_emoji) role.unicodeEmoji = payload.role.unicode_emoji
if (props.flags) role.flags = payload.role.flags
if ((props.botId || props.integrationId || props.subscriptionListingId) && payload.role.tags) {
if (props.tags && payload.role.tags) {
role.internalTags = {}
if (props.botId && payload.role.tags.bot_id) role.internalTags.botId = bot.transformers.snowflake(payload.role.tags.bot_id)
if (props.integrationId && payload.role.tags.integration_id)
role.internalTags.integrationId = bot.transformers.snowflake(payload.role.tags.integration_id)
if (props.subscriptionListingId && payload.role.tags.subscription_listing_id)
if (payload.role.tags.bot_id) role.internalTags.botId = bot.transformers.snowflake(payload.role.tags.bot_id)
if (payload.role.tags.integration_id) role.internalTags.integrationId = bot.transformers.snowflake(payload.role.tags.integration_id)
if (payload.role.tags.subscription_listing_id)
role.internalTags.subscriptionListingId = bot.transformers.snowflake(payload.role.tags.subscription_listing_id)
}
if (props.hoist || props.managed || props.mentionable) {
if (props.toggles) {
role.toggles = new RoleToggles(payload.role)
}
return bot.transformers.customizers.role(bot, payload.role, role)
}
export interface BaseRole {
/** The tags this role has */
tags?: {
/** The id of the bot this role belongs to */
botId?: bigint
/** The id of the integration this role belongs to */
integrationId?: bigint
/** Id of this role's subscription sku and listing. */
subscriptionListingId?: bigint
/** Whether this role is available for purchase. */
availableForPurchase?: boolean
/** Whether this is a guild's linked role */
guildConnections?: boolean
/** Whether this is the guild's premium subscriber role */
premiumSubscriber?: boolean
}
/** If this role is showed separately in the user listing */
hoist: boolean
/** Whether this role is managed by an integration */
managed: boolean
/** Whether this role is mentionable */
mentionable: boolean
/** Whether this is the guilds premium subscriber role */
premiumSubscriber: boolean
/** Whether this role is available for purchase. */
availableForPurchase: boolean
/** Whether this is a guild's linked role. */
guildConnections: boolean
}
export interface Role extends BaseRole {
/** Role id */
id: bigint
/** The guild id where this role is located. */
guildId: bigint
/** The compressed version of the boolean values on this role. */
toggles?: RoleToggles
/** If this role is showed separately in the user listing */
hoist: boolean
/** Permission bit set */
permissions: Permissions
/** Whether this role is managed by an integration */
managed: boolean
/** Whether this role is mentionable */
mentionable: boolean
/**
* Use role.tags
* @deprecated this is not deprecated, but this is here to prevent users from using this as this is an internal value open to breaking changes.
*/
internalTags?: {
/** The id of the bot this role belongs to */
botId?: bigint
/** The id of the integration this role belongs to */
integrationId?: bigint
/** Id of this role's subscription sku and listing. */
subscriptionListingId?: bigint
}
/** the role emoji hash */
icon?: bigint
/** Role name */
name: string
/** Integer representation of hexadecimal color code */
color: number
/** Position of this role */
position: number
/** role unicode emoji */
unicodeEmoji?: string
/** Role flags combined as a bitfield */
flags: RoleFlags
}

View File

@@ -1,11 +1,5 @@
import type {
DiscordScheduledEvent,
DiscordScheduledEventEntityMetadata,
ScheduledEventEntityType,
ScheduledEventPrivacyLevel,
ScheduledEventStatus,
} from '@discordeno/types'
import { type Bot, type User, iconHashToBigInt } from '../index.js'
import type { DiscordScheduledEvent } from '@discordeno/types'
import { type Bot, type ScheduledEvent, iconHashToBigInt } from '../index.js'
export function transformScheduledEvent(bot: Bot, payload: DiscordScheduledEvent): ScheduledEvent {
const props = bot.transformers.desiredProperties.scheduledEvent
@@ -30,38 +24,3 @@ export function transformScheduledEvent(bot: Bot, payload: DiscordScheduledEvent
return bot.transformers.customizers.scheduledEvent(bot, payload, scheduledEvent)
}
export interface ScheduledEvent {
/** the id of the scheduled event */
id: bigint
/** the guild id which the scheduled event belongs to */
guildId: bigint
/** the channel id in which the scheduled event will be hosted if specified */
channelId?: bigint
/** the id of the user that created the scheduled event */
creatorId?: bigint
/** the name of the scheduled event */
name: string
/** the description of the scheduled event */
description?: string
/** the time the scheduled event will start */
scheduledStartTime: number
/** the time the scheduled event will end if it does end. */
scheduledEndTime?: number
/** the privacy level of the scheduled event */
privacyLevel: ScheduledEventPrivacyLevel
/** the status of the scheduled event */
status: ScheduledEventStatus
/** the type of hosting entity associated with a scheduled event */
entityType: ScheduledEventEntityType
/** any additional id of the hosting entity associated with event */
entityId?: bigint
/** the location for the scheduled event */
location?: DiscordScheduledEventEntityMetadata['location']
/** the user that created the scheduled event */
creator?: User
/** the number of users subscribed to the scheduled event */
userCount?: number
/** the cover image hash of the scheduled event */
image?: bigint
}

View File

@@ -1,5 +1,5 @@
import type { DiscordSku, DiscordSkuType, SkuFlags } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { DiscordSku } from '@discordeno/types'
import type { Bot, Sku } from '../index.js'
export function transformSku(bot: Bot, payload: DiscordSku): Sku {
const props = bot.transformers.desiredProperties.sku
@@ -14,18 +14,3 @@ export function transformSku(bot: Bot, payload: DiscordSku): Sku {
return bot.transformers.customizers.sku(bot, payload, sku)
}
export interface Sku {
/** ID of SKU */
id: bigint
/** Type of SKU */
type: DiscordSkuType
/** ID of the parent application */
applicationId: bigint
/** Customer-facing name of your premium offering */
name: string
/** System-generated URL slug based on the SKU's name */
slug: string
/** SKU flags combined as a bitfield */
flags: SkuFlags
}

View File

@@ -1,5 +1,5 @@
import type { DiscordStageInstance } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { Bot, StageInstance } from '../index.js'
export function transformStageInstance(bot: Bot, payload: DiscordStageInstance): StageInstance {
const props = bot.transformers.desiredProperties.stageInstance
@@ -14,16 +14,3 @@ export function transformStageInstance(bot: Bot, payload: DiscordStageInstance):
return bot.transformers.customizers.stageInstance(bot, payload, stageInstance)
}
export interface StageInstance {
/** The topic of the Stage instance (1-120 characters) */
topic: string
/** The id of this Stage instance */
id: bigint
/** The guild id of the associated Stage channel */
guildId: bigint
/** The id of the associated Stage channel */
channelId: bigint
/** The id of the scheduled event for this Stage instance */
guildScheduledEventId?: bigint
}

View File

@@ -1,5 +1,5 @@
import type { BigString, DiscordInviteStageInstance, DiscordMember } from '@discordeno/types'
import type { Bot, Member } from '../index.js'
import type { Bot, InviteStageInstance } from '../index.js'
export function transformInviteStageInstance(bot: Bot, payload: DiscordInviteStageInstance & { guildId: BigString }): InviteStageInstance {
const props = bot.transformers.desiredProperties.inviteStageInstance
@@ -21,14 +21,3 @@ export function transformInviteStageInstance(bot: Bot, payload: DiscordInviteSta
return bot.transformers.customizers.inviteStageInstance(bot, payload, inviteStageInstance)
}
export interface InviteStageInstance {
/** The members speaking in the Stage */
members: Partial<Member>[]
/** The number of users in the Stage */
participantCount: number
/** The number of users speaking in the Stage */
speakerCount: number
/** The topic of the Stage instance (1-120 characters) */
topic: string
}

View File

@@ -1,5 +1,5 @@
import type { DiscordSticker, DiscordStickerPack, StickerFormatTypes, StickerTypes } from '@discordeno/types'
import type { Bot, User } from '../index.js'
import type { DiscordSticker, DiscordStickerPack } from '@discordeno/types'
import type { Bot, Sticker, StickerPack } from '../index.js'
export function transformSticker(bot: Bot, payload: DiscordSticker): Sticker {
const props = bot.transformers.desiredProperties.sticker
@@ -33,37 +33,3 @@ export function transformStickerPack(bot: Bot, payload: DiscordStickerPack): Sti
return bot.transformers.customizers.stickerPack(bot, payload, pack)
}
export interface Sticker {
/** [Id of the sticker](https://discord.com/developers/docs/reference#image-formatting) */
id: bigint
/** Id of the pack the sticker is from */
packId?: bigint
/** Name of the sticker */
name: string
/** Description of the sticker */
description: string
/** a unicode emoji representing the sticker's expression */
tags: string
/** [type of sticker](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types) */
type: StickerTypes
/** [Type of sticker format](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types) */
formatType: StickerFormatTypes
/** Whether or not the sticker is available */
available?: boolean
/** Id of the guild that owns this sticker */
guildId?: bigint
/** The user that uploaded the sticker */
user?: User
/** A sticker's sort order within a pack */
sortValue?: number
}
export interface StickerPack {
coverStickerId?: bigint
bannerAssetId?: bigint
id: bigint
name: string
description: string
stickers: Sticker[]
skuId: bigint
}

View File

@@ -1,5 +1,5 @@
import type { DiscordTeam, DiscordTeamMemberRole, TeamMembershipStates } from '@discordeno/types'
import { type Bot, type User, iconHashToBigInt } from '../index.js'
import type { DiscordTeam } from '@discordeno/types'
import { type Bot, type Team, iconHashToBigInt } from '../index.js'
export function transformTeam(bot: Bot, payload: DiscordTeam): Team {
const id = bot.transformers.snowflake(payload.id)
@@ -19,16 +19,3 @@ export function transformTeam(bot: Bot, payload: DiscordTeam): Team {
return bot.transformers.customizers.team(bot, payload, team)
}
export interface Team {
icon?: bigint | undefined
id: bigint
name: string
ownerUserId: bigint
members: Array<{
membershipState: TeamMembershipStates
teamId: bigint
user: User
role: DiscordTeamMemberRole
}>
}

View File

@@ -1,5 +1,5 @@
import type { DiscordTemplate } from '@discordeno/types'
import type { Bot, User } from '../index.js'
import type { Bot, Template } from '../index.js'
export function transformTemplate(bot: Bot, payload: DiscordTemplate): Template {
const template = {
@@ -18,17 +18,3 @@ export function transformTemplate(bot: Bot, payload: DiscordTemplate): Template
return bot.transformers.customizers.template(bot, payload, template)
}
export interface Template {
description?: string | null
isDirty?: boolean
name: string
creatorId: bigint
createdAt: number
code: string
usageCount: number
creator: User
updatedAt: number
sourceGuildId: bigint
serializedSourceGuild: NonNullable<DiscordTemplate['serialized_source_guild']>
}

View File

@@ -1,5 +1,5 @@
import type { DiscordThreadMember } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { Bot, ThreadMember, ThreadMemberGuildCreate } from '../index.js'
import type { DiscordThreadMemberGuildCreate } from '../typings.js'
export function transformThreadMember(bot: Bot, payload: DiscordThreadMember): ThreadMember {
@@ -20,14 +20,3 @@ export function transformThreadMemberGuildCreate(bot: Bot, payload: DiscordThrea
return bot.transformers.customizers.threadMemberGuildCreate(bot, payload, threadMember)
}
export interface ThreadMember {
id?: bigint
userId?: bigint
flags: number
joinTimestamp: number
}
export interface ThreadMemberGuildCreate {
joinTimestamp: number
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
import type { DiscordUser, PremiumTypes } from '@discordeno/types'
import type { DiscordUser } from '@discordeno/types'
import { iconHashToBigInt } from '@discordeno/utils'
import { type AvatarDecorationData, type Bot, ToggleBitfield, UserToggles } from '../index.js'
import { type Bot, ToggleBitfield, type User, UserToggles } from '../index.js'
const baseUser: Partial<User> & BaseUser = {
const baseUser = {
get tag() {
return `${this.username}#${this.discriminator}`
},
@@ -18,15 +18,13 @@ const baseUser: Partial<User> & BaseUser = {
get verified() {
return !!this.toggles?.has('verified')
},
}
} as User
export function transformUser(bot: Bot, payload: DiscordUser): User {
const user: User = Object.create(baseUser)
const props = bot.transformers.desiredProperties.user
if (props.bot || props.system || props.mfaEnabled || props.verified) {
user.toggles = new UserToggles(payload)
}
if (props.toggles) user.toggles = new UserToggles(payload)
if (props.flags) user.flags = new ToggleBitfield(payload.flags)
if (props.publicFlags) user.publicFlags = new ToggleBitfield(payload.public_flags)
if (props.id && payload.id) user.id = bot.transformers.snowflake(payload.id)
@@ -44,47 +42,3 @@ export function transformUser(bot: Bot, payload: DiscordUser): User {
return bot.transformers.customizers.user(bot, payload, user)
}
export interface BaseUser {
/** The user tag in the form of username#discriminator */
tag: string
/** Whether the user belongs to an OAuth2 application */
bot: boolean
/** Whether the user is an Official Discord System user (part of the urgent message system) */
system: boolean
/** Whether the user has two factor enabled on their account */
mfaEnabled: boolean
/** Whether the email on this account has been verified */
verified: boolean
}
export interface User extends BaseUser {
/** Compressed version of all the booleans on a user. */
toggles?: UserToggles
/** The user's username, not unique across the platform */
username: string
/** The user's display name, if it is set. For bots, this is the application name */
globalName?: string
/** The user's chosen language option */
locale?: string
/** The flags on a user's account */
flags?: ToggleBitfield
/** The type of Nitro subscription on a user's account */
premiumType?: PremiumTypes
/** The public flags on a user's account */
publicFlags?: ToggleBitfield
/** the user's banner color encoded as an integer representation of hexadecimal color code */
accentColor?: number
/** The user's id */
id: bigint
/** The user's discord-tag */
discriminator: string
/** The user's avatar hash */
avatar?: bigint
/** The user's email */
email?: string
/** the user's banner, or null if unset */
banner?: bigint
/** data for the user's avatar decoration */
avatarDecorationData?: AvatarDecorationData
}

View File

@@ -1,5 +1,5 @@
import type { DiscordVoiceRegion } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { Bot, VoiceRegion } from '../index.js'
export function transformVoiceRegion(bot: Bot, payload: DiscordVoiceRegion): VoiceRegion {
const voiceRegion = {
@@ -12,11 +12,3 @@ export function transformVoiceRegion(bot: Bot, payload: DiscordVoiceRegion): Voi
return bot.transformers.customizers.voiceRegion(bot, payload, voiceRegion)
}
export interface VoiceRegion {
id: string
name: string
custom: boolean
optimal: boolean
deprecated: boolean
}

View File

@@ -1,5 +1,5 @@
import type { DiscordVoiceState } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { Bot, VoiceState } from '../index.js'
import { VoiceStateToggles } from './toggles/voice.js'
export function transformVoiceState(bot: Bot, payload: { voiceState: DiscordVoiceState } & { guildId: bigint }): VoiceState {
@@ -16,12 +16,3 @@ export function transformVoiceState(bot: Bot, payload: { voiceState: DiscordVoic
return bot.transformers.customizers.voiceState(bot, payload.voiceState, voiceState)
}
export interface VoiceState {
requestToSpeakTimestamp?: number
channelId?: bigint
guildId: bigint
toggles: VoiceStateToggles
sessionId: string
userId: bigint
}

View File

@@ -1,5 +1,5 @@
import type { DiscordWebhook, WebhookTypes } from '@discordeno/types'
import { type Bot, type Channel, type Guild, type User, iconHashToBigInt } from '../index.js'
import type { DiscordWebhook } from '@discordeno/types'
import { type Bot, type Webhook, iconHashToBigInt } from '../index.js'
export function transformWebhook(bot: Bot, payload: DiscordWebhook): Webhook {
const props = bot.transformers.desiredProperties.webhook
@@ -29,31 +29,3 @@ export function transformWebhook(bot: Bot, payload: DiscordWebhook): Webhook {
return bot.transformers.customizers.webhook(bot, payload, webhook)
}
export interface Webhook {
/** The type of the webhook */
type: WebhookTypes
/** The secure token of the webhook (returned for Incoming Webhooks) */
token?: string
/** The url used for executing the webhook (returned by the webhooks OAuth2 flow) */
url?: string
/** The id of the webhook */
id: bigint
/** The guild id this webhook is for */
guildId?: bigint
/** The channel id this webhook is for */
channelId?: bigint
/** The user this webhook was created by (not returned when getting a webhook with its token) */
user?: User
/** The default name of the webhook */
name?: string
/** The default user avatar hash of the webhook */
avatar?: bigint
/** The bot/OAuth2 application that created this webhook */
applicationId?: bigint
/** The guild of the channel that this webhook is following (returned for Channel Follower Webhooks) */
sourceGuild?: Partial<Guild>
/** The channel that this webhook is following (returned for Channel Follower Webhooks) */
sourceChannel?: Partial<Channel>
}

View File

@@ -1,5 +1,5 @@
import type { DiscordWelcomeScreen } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { Bot, WelcomeScreen } from '../index.js'
export function transformWelcomeScreen(bot: Bot, payload: DiscordWelcomeScreen): WelcomeScreen {
const welcomeScreen = {
@@ -14,13 +14,3 @@ export function transformWelcomeScreen(bot: Bot, payload: DiscordWelcomeScreen):
return bot.transformers.customizers.welcomeScreen(bot, payload, welcomeScreen)
}
export interface WelcomeScreen {
description?: string
welcomeChannels: Array<{
channelId: bigint
description: string
emojiId?: bigint
emojiName?: string
}>
}

View File

@@ -1,5 +1,5 @@
import type { DiscordGuildWidget } from '@discordeno/types'
import { type Bot, iconHashToBigInt } from '../index.js'
import { type Bot, type GuildWidget, iconHashToBigInt } from '../index.js'
export function transformWidget(bot: Bot, payload: DiscordGuildWidget): GuildWidget {
const widget = {
@@ -24,23 +24,3 @@ export function transformWidget(bot: Bot, payload: DiscordGuildWidget): GuildWid
return bot.transformers.customizers.widget(bot, payload, widget)
}
export interface GuildWidget {
id: bigint
name: string
members: Array<{
id: bigint
username: string
discriminator: string
avatar?: bigint
status: string
avatarUrl: string
}>
channels: Array<{
id: bigint
name: string
position: number
}>
instant_invite: string
presenceCount: number
}

View File

@@ -1,5 +1,5 @@
import type { DiscordGuildWidgetSettings } from '@discordeno/types'
import type { Bot } from '../index.js'
import type { Bot, GuildWidgetSettings } from '../index.js'
export function transformWidgetSettings(bot: Bot, payload: DiscordGuildWidgetSettings): GuildWidgetSettings {
const widget = {
@@ -9,8 +9,3 @@ export function transformWidgetSettings(bot: Bot, payload: DiscordGuildWidgetSet
return bot.transformers.customizers.widgetSettings(bot, payload, widget)
}
export interface GuildWidgetSettings {
channelId?: string
enabled: boolean
}

View File

@@ -22,8 +22,7 @@ import {
type TextStyles,
} from '@discordeno/types'
import type * as handlers from './handlers/index.js'
import type { ApplicationCommandOptionChoice } from './transformers/applicationCommandOptionChoice.js'
import type { Embed } from './transformers/embed.js'
import type { ApplicationCommandOptionChoice, Embed } from './transformers/index.js'
export function isContextApplicationCommand(command: CreateApplicationCommand): command is CreateContextApplicationCommand {
return command.type === ApplicationCommandTypes.Message || command.type === ApplicationCommandTypes.User

View File

@@ -0,0 +1 @@
import('../dist/esm/bin/index.js')

View File

@@ -1 +0,0 @@
import('../dist/bin/index.js')

View File

@@ -7,7 +7,7 @@
"require": "./dist/cjs/index.cjs",
"types": "./dist/types/index.d.ts"
},
"bin": "./bin/disocrdeno.js",
"bin": "./bin/discordeno.js",
"types": "./dist/types/index.d.ts",
"type": "module",
"license": "Apache-2.0",
@@ -24,6 +24,7 @@
"test:unit-coverage": "c8 mocha --no-warnings 'tests/**/*.spec.ts'",
"test:unit": "c8 --r lcov mocha --no-warnings 'tests/**/*.spec.ts' && node ../../scripts/coveragePathFixing.js discordeno",
"test:deno-unit": "swc --strip-leading-paths tests --delete-dir-on-start --out-dir denoTestsDist && node ../../scripts/fixDenoTestExtension.js && deno test -A --import-map ../../denoImportMap.json denoTestsDist",
"test:bun-unit": "node ../../scripts/fixBunTestExtension.js && bun test bunTestsDist",
"test:unit:watch": "mocha --no-warnings --watch --parallel 'tests/**/*.spec.ts'",
"test:type": "tsc --noEmit",
"test:test-type": "tsc --project tests/tsconfig.json"
@@ -34,7 +35,9 @@
"@discordeno/rest": "19.0.0-alpha.1",
"@discordeno/types": "19.0.0-beta.1",
"@discordeno/utils": "19.0.0-beta.1",
"commander": "^12.1.0"
"commander": "^12.1.0",
"find-up": "^7.0.0",
"typescript": "^5.5.3"
},
"devDependencies": {
"@biomejs/biome": "^1.8.0",
@@ -49,7 +52,6 @@
"mocha": "^10.5.1",
"sinon": "^18.0.0",
"ts-node": "^10.9.2",
"tsconfig": "*",
"typescript": "^5.5.3"
"tsconfig": "*"
}
}

View File

@@ -0,0 +1,88 @@
import { unlink, writeFile } from 'node:fs/promises'
import { dirname } from 'node:path'
import { pathToFileURL } from 'node:url'
import { type TransformersDesiredProprieties, createDesiredProprietiesObject, gray } from '@discordeno/bot'
import type { RecursivePartial } from '@discordeno/types'
import { findUp } from 'find-up'
import ts from 'typescript'
export enum DesiredProprietiesBehavior {
Remove,
TypeAsNever,
}
export const typescriptOptions: ts.CompilerOptions = {
target: ts.ScriptTarget.ES2022,
module: ts.ModuleKind.Node16,
moduleResolution: ts.ModuleResolutionKind.Node16,
skipLibCheck: true,
skipDefaultLibCheck: true,
strict: true,
}
export function defineConfig(config: RecursivePartial<DiscordenoConfig>): DiscordenoConfig {
return {
desiredProperties: {
behavior: config.desiredProperties?.behavior ?? DesiredProprietiesBehavior.TypeAsNever,
properties: createDesiredProprietiesObject(config.desiredProperties?.properties ?? {}),
},
}
}
export async function findConfig(path?: string): Promise<DiscordenoConfig> {
const fileToSearch = path ?? ['discordeno.config.js', 'discordeno.config.mjs', 'discordeno.config.ts', 'discordeno.config.mts']
const file = await findUp(fileToSearch, { allowSymlinks: true, type: 'file' })
if (!file) {
throw new Error('Could not find the config file, please pass the config file explicitly')
}
console.log(gray(`Found config file at: ${file}`))
const module = await importConfig(file)
const config = module.default
if (!config || typeof config !== 'object') {
throw new Error("The config could not be found or it isn't an object")
}
return config
}
async function importConfig(file: string) {
if (!file.endsWith('.ts') && !file.endsWith('.mts')) {
// We need to convert to a file:// url to prevent a nodejs error on Windows
return import(pathToFileURL(file).href)
}
// for .ts files we need to build them, write them to disk (for relative imports) and then remove them
const fileName = `${dirname(file)}/discordeno-${Date.now()}.config.mjs`
try {
await writeFile(fileName, buildConfig(file))
// We need to convert to a file:// url to prevent a nodejs error on Windows
return await import(pathToFileURL(fileName).href)
} finally {
await unlink(fileName)
}
}
function buildConfig(path: string) {
const createdFiles: Record<string, string> = {}
const program = ts.createProgram([path], typescriptOptions)
program.emit(undefined, (file, text) => (createdFiles[file] = text))
const outFileName = path.replaceAll('\\', '/').replace(/\.m?[tj]s/i, '.js')
return createdFiles[outFileName]
}
export interface DiscordenoConfig {
desiredProperties: {
behavior: DesiredProprietiesBehavior
properties: RecursivePartial<TransformersDesiredProprieties>
}
}

View File

@@ -0,0 +1,115 @@
import type { DiscordenoConfig } from '../config.js'
/** Mapping to all the dependencies a specific getter has */
const computedDesiredProprieties = {
channel: {
archived: ['toggles'],
invitable: ['toggles'],
locked: ['toggles'],
nsfw: ['toggles'],
newlyCreated: ['toggles'],
managed: ['toggles'],
threadMetadata: ['toggles', 'archiveTimestamp', 'createTimestamp', 'autoArchiveDuration'],
},
guild: {
threads: ['channels'],
features: ['toggles'],
},
interaction: {
respond: ['type', 'token', 'id'],
edit: ['type', 'token', 'id'],
deferEdit: ['type', 'token', 'id'],
defer: ['type', 'token', 'id'],
delete: ['type', 'token'],
},
member: {
deaf: ['toggles'],
mute: ['toggles'],
pending: ['toggles'],
flags: ['toggles'],
didRejoin: ['toggles'],
startedOnboarding: ['toggles'],
bypassesVerification: ['toggles'],
completedOnboarding: ['toggles'],
},
message: {
crossposted: ['flags'],
ephemeral: ['flags'],
failedToMentionSomeRolesInThread: ['flags'],
hasThread: ['flags'],
isCrosspost: ['flags'],
loading: ['flags'],
mentionedUserIds: ['mentions'],
mentionEveryone: ['bitfield'],
pinned: ['bitfield'],
sourceMessageDeleted: ['flags'],
suppressEmbeds: ['flags'],
suppressNotifications: ['flags'],
timestamp: ['id'],
tts: ['bitfield'],
urgent: ['flags'],
},
role: {
tags: ['tags', 'toggles'],
hoist: ['toggles'],
managed: ['toggles'],
mentionable: ['toggles'],
premiumSubscriber: ['toggles'],
availableForPurchase: ['toggles'],
guildConnections: ['toggles'],
},
user: {
tag: ['username', 'discriminator'],
bot: ['toggles'],
system: ['toggles'],
mfaEnabled: ['toggles'],
verified: ['toggles'],
},
}
export function isPropertyDesired(config: DiscordenoConfig, interfaceName: string, memberName: string): boolean {
const desiredProperties = config.desiredProperties.properties
const name = pascalCaseToCamelCase(interfaceName)
const interfaceProps = desiredProperties[name as keyof typeof desiredProperties]
const computedProps = computedDesiredProprieties[name as keyof typeof computedDesiredProprieties]
// This interface does not support desired proprieties, so we include them
if (!interfaceProps) {
return true
}
const isPropDesired = interfaceProps[memberName as keyof typeof interfaceProps] ?? false
// If this interface has some computed props, and this member is one of the ones that is in fact computed, check it's dependencies
if (computedProps) {
const dependencies: string[] = computedProps[memberName as keyof typeof computedProps]
if (dependencies) {
return dependencies.every((x) => {
// To avoid an infinite loop we need to check that we don't call isPropertyDesired on this same member
if (x === memberName) return isPropDesired
return isPropertyDesired(config, interfaceName, x)
})
}
}
return isPropDesired
}
export function getPropertyDependencies(interfaceName: string, memberName: string): string[] | undefined {
const name = pascalCaseToCamelCase(interfaceName)
const computedProps = computedDesiredProprieties[name as keyof typeof computedDesiredProprieties]
if (!computedProps) return undefined
return computedProps[memberName as keyof typeof computedProps]
}
function pascalCaseToCamelCase(str: string) {
if (str.length === 0) return str
return `${str[0].toLowerCase()}${str.slice(1)}`
}

View File

@@ -0,0 +1,55 @@
import type { WriteStream } from 'node:fs'
import type ts from 'typescript'
export function writeInterfaceMember(stream: WriteStream, name: string, type: string, optional: boolean) {
stream.write(` ${name}${optional ? '?' : ''}: ${type}\n`)
}
export function writeJSDoc(stream: WriteStream, docs: ts.SymbolDisplayPart[], jsDocTags: ts.JSDocTagInfo[], ident = ''): void {
const withDocs = docs.length > 0
const withJSdoc = jsDocTags.length > 0
if (!withDocs && !withJSdoc) return
stream.write(`${ident}/**`)
if (withDocs && withJSdoc) {
stream.write(`\n${ident} *`)
}
if (withDocs) {
writeSymbolDisplayParts(stream, docs, ident)
}
for (const jsDoc of jsDocTags) {
if (withDocs) {
stream.write(`\n${ident} *`)
}
stream.write(`\n${ident} *`)
stream.write(` @${jsDoc.name}\n${ident} *`)
if (jsDoc.text) {
writeSymbolDisplayParts(stream, jsDoc.text, ident)
}
}
if (withJSdoc) {
stream.write(`\n${ident}`)
}
stream.write(' */\n')
}
function writeSymbolDisplayParts(stream: WriteStream, documentation: ts.SymbolDisplayPart[], ident: string): void {
const docs = documentation.reduce((acc, cur) => `${acc}${cur.kind === 'linkText' ? ' | ' : ''}${cur.text}`, '')
const splitted = docs.split('\n')
stream.write(' ')
stream.write(splitted[0])
for (const text of splitted.slice(1)) {
stream.write(`\n${ident} * `)
stream.write(text)
}
}

View File

@@ -0,0 +1,123 @@
import assert from 'node:assert'
import { type WriteStream, createWriteStream } from 'node:fs'
import ts from 'typescript'
import { DesiredProprietiesBehavior, type DiscordenoConfig, typescriptOptions } from '../config.js'
import { getPropertyDependencies, isPropertyDesired } from './desiredProperty.js'
import { writeInterfaceMember, writeJSDoc } from './emitter.js'
export const autoGeneratedNote = '// <auto-generated>\n'
export function generateNewFile(config: DiscordenoConfig, fileName: string, outFile: string) {
const program = ts.createProgram([fileName], typescriptOptions)
// The type checker will give us find more about symbols
const checker = program.getTypeChecker()
const sourceFile = program.getSourceFile(fileName)
if (!sourceFile) {
throw new Error('Cannot process a undefined file')
}
const stream = createWriteStream(outFile, 'utf-8')
stream.write(autoGeneratedNote)
ts.forEachChild(sourceFile, (node) => processChild(stream, config, checker, node))
}
function processChild(writeStream: WriteStream, config: DiscordenoConfig, checker: ts.TypeChecker, node: ts.Node): void {
if (ts.isImportDeclaration(node)) {
processImportNode(node, writeStream)
return
}
if (ts.isInterfaceDeclaration(node)) {
processInterfaceDeclarationNode(node, config, checker, writeStream)
return
}
}
function processInterfaceDeclarationNode(node: ts.InterfaceDeclaration, config: DiscordenoConfig, checker: ts.TypeChecker, writeStream: WriteStream) {
const symbol = checker.getSymbolAtLocation(node.name)
if (!symbol) return
assert(symbol.members)
const interfaceName = symbol.getName()
writeStream.write('\n')
writeJSDoc(writeStream, symbol.getDocumentationComment(checker), symbol.getJsDocTags(checker))
writeStream.write(`export interface ${interfaceName} {\n`)
// Generate the interface in the output file
for (const member of symbol.members.values()) {
const memberName = member.getName()
const valueDeclaration = member.valueDeclaration
assert(valueDeclaration)
const valueDeclarationChildren = valueDeclaration.getChildren()
const typeNode = valueDeclarationChildren.find((x) => ts.isTypeNode(x))
assert(typeNode)
// Since we are getting the type directly from the sourceFile it may have a trailing space, so we remove them
const typeText = typeNode.getFullText().trim()
const jsDoc = member.getJsDocTags(checker)
const docs = member.getDocumentationComment(checker)
const isOptional = !!(member.getFlags() & ts.SymbolFlags.Optional)
const isInternal = !!jsDoc.find((x) => x.name === 'internal')
// If the propriety is internal then we don't want to apply the desired propriety logic to it
if (isInternal || isPropertyDesired(config, interfaceName, memberName)) {
writeJSDoc(writeStream, docs, jsDoc, ' ')
writeInterfaceMember(writeStream, memberName, typeText, isOptional)
continue
}
// The property is undesired
handleUndesiredProperty(writeStream, config, docs, jsDoc, interfaceName, memberName, typeText, isOptional)
}
writeStream.write('}\n')
}
function processImportNode(node: ts.ImportDeclaration, writeStream: WriteStream) {
let importText = node.getFullText().trim()
// We manually add the newline afterwards, so we need to remove it from here
if (importText.startsWith('\n')) {
importText = importText.substring(1)
}
writeStream.write(importText)
writeStream.write('\n')
}
function handleUndesiredProperty(
stream: WriteStream,
config: DiscordenoConfig,
docs: ts.SymbolDisplayPart[],
jsDoc: ts.JSDocTagInfo[],
interfaceName: string,
memberName: string,
typeText: string,
isOptional: boolean,
) {
if (config.desiredProperties.behavior === DesiredProprietiesBehavior.Remove) return
const dependencies = getPropertyDependencies(interfaceName, memberName)
const additionalJsDocTag: ts.JSDocTagInfo = {
name: 'remarks',
text: [
{
kind: 'text',
text: `This property is not desired according to your Desired Properties configuration.\n\nOriginal type: ${typeText}${dependencies ? `\n\nThis value requires other values to be enabled, those are: ${dependencies.join(', ')}` : ''}`,
},
],
}
jsDoc.push(additionalJsDocTag)
writeJSDoc(stream, docs, jsDoc, ' ')
writeInterfaceMember(stream, memberName, 'never', isOptional)
}

View File

@@ -1,4 +1,9 @@
import { readFile, rename, stat } from 'node:fs/promises'
import { Command } from 'commander'
import { findUp } from 'find-up'
import { findConfig } from './config.js'
import { autoGeneratedNote, generateNewFile } from './generate/typescript.js'
const program = new Command()
program.name('discordeno').description('CLI to discordeno utilities').version('0.1.0')
@@ -6,6 +11,42 @@ program.name('discordeno').description('CLI to discordeno utilities').version('0
program
.command('generate')
.description('Generate types/schema for discordeno')
.action(() => {})
.option('-c, --config [path]', 'Path to the config file')
.action(async (options) => {
const config = await findConfig(options.config)
const typesFile = await findUp('node_modules/@discordeno/bot/dist/types/transformers/types.d.ts', { allowSymlinks: true, type: 'file' })
if (!typesFile) {
throw new Error('Could not find @discordeno/bot transformer types file.')
}
// We use a .old file to preserve the original file in case we need it in the future
// The reason for this is because when we write the updated .d.ts file we may lose some type information, so we still need the original file
// However if a user updates the package version we want to use the updated version, so for this reason we check for "// <auto-generated>" at the beginning of the file
const oldTypesFile = typesFile.replace('.d.ts', '.old.d.ts')
const typesFileContent = await readFile(typesFile, 'utf-8')
const startFile = typesFileContent.slice(0, autoGeneratedNote.length)
// If we found that the current types.d.ts file has been auto generated then use the .old one
const usingOldFile = startFile === autoGeneratedNote
const fileToUse = usingOldFile ? oldTypesFile : typesFile
const fileToUseStat = await stat(fileToUse).catch(() => null)
// If somehow the file we decided to use does not exist we error out
if (!fileToUseStat) {
throw new Error('Could not find a valid file to use')
}
// If we are using the .d.ts file rename it to .old.d.ts so we can use it a later point
if (!usingOldFile) {
await rename(typesFile, oldTypesFile)
}
generateNewFile(config, oldTypesFile, typesFile)
})
program.parse()

View File

@@ -1 +1,2 @@
export * from '@discordeno/bot'
export * from './bin/config.js'

View File

@@ -0,0 +1,91 @@
import { expect } from 'chai'
import { findUp } from 'find-up'
import { describe, it } from 'mocha'
import ts from 'typescript'
import { typescriptOptions } from '../src/bin/config.js'
import { getPropertyDependencies, isPropertyDesired } from '../src/bin/generate/desiredProperty.js'
import { defineConfig } from '../src/index.js'
describe('discordeno generate', () => {
it('will emit without errors', async function () {
// Mocha will crash if this takes more then 2s
// Deno does not have the timeout function
// Bun does not give a this object at all
if (this?.timeout) {
// we set the timeout to 20s, typescript can be slow at doing the entire type checking
this.timeout(20_000)
}
const typesFile = await findUp('packages/bot/dist/types/transformers/types.d.ts', {
allowSymlinks: true,
})
expect(typesFile).to.exist
if (!typesFile) {
throw new Error('Transformers types file not found!')
}
const program = ts.createProgram([typesFile], {
...typescriptOptions,
noEmit: true,
})
// Check if typescript is producing type errors for the program
const emitResult = program.emit()
const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics)
expect(allDiagnostics).to.be.an('array').that.is.empty
})
it('can get propriety dependencies', () => {
const deps = getPropertyDependencies('member', 'mute')
expect(deps).to.have.members(['toggles'])
})
it('can get desired status for a prop', () => {
const config = defineConfig({
desiredProperties: {
properties: {
channel: {
id: true,
},
},
},
})
// desired
const idProp = isPropertyDesired(config, 'channel', 'id')
expect(idProp).to.be.equal(true)
// not desired
const nameProp = isPropertyDesired(config, 'channel', 'name')
expect(nameProp).to.be.equal(false)
})
it('can get computed desired status for a prop', () => {
const config = defineConfig({
desiredProperties: {
properties: {
interaction: {
type: true,
token: true,
},
},
},
})
// missing all deps
const threadMetadata = isPropertyDesired(config, 'channel', 'threadMetadata')
expect(threadMetadata).to.be.equal(false)
// missing one dep
const respond = isPropertyDesired(config, 'interaction', 'respond')
expect(respond).to.be.equal(false)
// having all deps
const interactionDelete = isPropertyDesired(config, 'interaction', 'delete')
expect(interactionDelete).to.be.equal(true)
})
})

View File

@@ -1,7 +1,6 @@
import type {
AddDmRecipientOptions,
AddGuildMemberOptions,
ApplicationCommandPermissions,
AtLeastOne,
BeginGuildPrune,
BigString,
@@ -10,6 +9,7 @@ import type {
CamelizedDiscordActiveThreads,
CamelizedDiscordApplication,
CamelizedDiscordApplicationCommand,
CamelizedDiscordApplicationCommandPermissions,
CamelizedDiscordApplicationRoleConnection,
CamelizedDiscordArchivedThreads,
CamelizedDiscordAuditLog,
@@ -1013,7 +1013,7 @@ export interface RestManager {
guildId: BigString,
commandId: BigString,
bearerToken: string,
options: ApplicationCommandPermissions[],
options: CamelizedDiscordApplicationCommandPermissions[],
) => Promise<CamelizedDiscordGuildApplicationCommandPermissions>
/**
* Edits an automod rule.

View File

@@ -2,8 +2,8 @@
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"target": "ES2022",
"module": "Node16",
"rootDir": "${configDir}/src",
"outDir": "${configDir}/dist",
"tsBuildInfoFile": "${configDir}/dist/.tsbuildinfo",
@@ -14,7 +14,7 @@
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "node",
"moduleResolution": "Node16",
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true,

View File

@@ -2836,31 +2836,34 @@ export interface DiscordTemplate {
/** The Id of the guild this template is based on */
source_guild_id: string
/** The guild snapshot this template contains */
serialized_source_guild: Omit<
PickPartial<
DiscordGuild,
| 'name'
| 'description'
| 'verification_level'
| 'default_message_notifications'
| 'explicit_content_filter'
| 'preferred_locale'
| 'afk_timeout'
| 'channels'
| 'afk_channel_id'
| 'system_channel_id'
| 'system_channel_flags'
>,
'roles'
> & {
roles: Array<
Omit<PickPartial<DiscordRole, 'name' | 'color' | 'hoist' | 'mentionable' | 'permissions' | 'icon' | 'unicode_emoji'>, 'id'> & { id: number }
>
}
/** Whether the template has un-synced changes */
serialized_source_guild: DiscordTemplateSerializedSourceGuild
is_dirty: boolean | null
}
export type DiscordTemplateSerializedSourceGuild = Omit<
PickPartial<
DiscordGuild,
| 'name'
| 'description'
| 'verification_level'
| 'default_message_notifications'
| 'explicit_content_filter'
| 'preferred_locale'
| 'afk_timeout'
| 'channels'
| 'afk_channel_id'
| 'system_channel_id'
| 'system_channel_flags'
>,
'roles'
> & {
roles: Array<
Omit<PickPartial<DiscordRole, 'name' | 'color' | 'hoist' | 'mentionable' | 'permissions' | 'icon' | 'unicode_emoji'>, 'id'> & {
id: number
}
>
}
/** https://discord.com/developers/docs/topics/gateway#guild-member-add */
export interface DiscordGuildMemberAdd extends DiscordMemberWithUser {
/** id of the guild */
@@ -3385,7 +3388,7 @@ export interface DiscordGuildOnboardingPromptOption {
/** Title of the option */
title: string
/** Description of the option */
description: string | undefined
description: string | null
}
/** https://discord.com/developers/docs/resources/guild#guild-onboarding-object-prompt-types */

View File

@@ -23,7 +23,6 @@ import type {
} from './discord.js'
import type {
AllowedMentionsTypes,
ApplicationCommandPermissionTypes,
ApplicationCommandTypes,
ApplicationFlags,
AuditLogEvents,
@@ -1167,16 +1166,6 @@ export interface EditMessage {
components?: MessageComponents
}
/** https://discord.com/developers/docs/interactions/application-commands#edit-application-command-permissions */
export interface ApplicationCommandPermissions {
/** The id of the role or user */
id: string
/** Role or User */
type: ApplicationCommandPermissionTypes
/** `true` to allow, `false`, to disallow */
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 */

View File

@@ -1162,3 +1162,5 @@ export type Snakelize<T> = T extends any[]
: T
export type PickPartial<T, K extends keyof T> = { [P in keyof T]?: T[P] | undefined } & { [P in K]: T[P] }
export type RecursivePartial<T> = T extends object ? { [K in keyof T]?: RecursivePartial<T[K]> } : Partial<T>

View File

@@ -14,7 +14,6 @@ for await (const dir of dirs) {
`denoTestsDist${dir}/${file.slice(-8) === '.spec.js' ? `${file.slice(0, -7)}test.js` : file}`,
content
.replace(/src\//g, 'dist/esm/')
.replace(/\.ts/g, '.js')
.replace(/describe\.skip/g, 'describe.ignore')
.replace(/it\.skip/g, 'it.ignore'),
)

View File

@@ -14,7 +14,7 @@
"outputs": ["coverage/**"]
},
"test:unit": {
"dependsOn": ["^build"],
"dependsOn": ["^build", "build", "build:type"],
"outputs": ["coverage/**"]
},
"test:integration": {
@@ -25,11 +25,11 @@
"dependsOn": ["^build:type"]
},
"test:deno-unit": {
"dependsOn": ["build", "^build"],
"dependsOn": ["^build", "build", "build:type"],
"outputs": ["denoTestsDist/**"]
},
"test:bun-unit": {
"dependsOn": ["^build"],
"dependsOn": ["^build", "build", "build:type"],
"outputs": ["bunTestsDist/**"]
},
"test:e2e": {

View File

@@ -1524,6 +1524,7 @@ __metadata:
dependencies:
"@biomejs/biome": "npm:^1.8.0"
chokidar-cli: "npm:^3.0.0"
discordeno: "npm:19.0.0-alpha.1"
husky: "npm:^9.0.11"
lint-staged: "npm:^15.2.7"
turbo: "npm:^2.0.6"
@@ -1533,7 +1534,7 @@ __metadata:
languageName: unknown
linkType: soft
"discordeno@workspace:packages/discordeno":
"discordeno@npm:19.0.0-alpha.1, discordeno@workspace:packages/discordeno":
version: 0.0.0-use.local
resolution: "discordeno@workspace:packages/discordeno"
dependencies:
@@ -1552,13 +1553,14 @@ __metadata:
c8: "npm:^9.1.0"
chai: "npm:^5.1.1"
commander: "npm:^12.1.0"
find-up: "npm:^7.0.0"
mocha: "npm:^10.5.1"
sinon: "npm:^18.0.0"
ts-node: "npm:^10.9.2"
tsconfig: "npm:*"
typescript: "npm:^5.5.3"
bin:
discordeno: ./bin/disocrdeno.js
discordeno: ./bin/discordeno.js
languageName: unknown
linkType: soft
@@ -1813,6 +1815,17 @@ __metadata:
languageName: node
linkType: hard
"find-up@npm:^7.0.0":
version: 7.0.0
resolution: "find-up@npm:7.0.0"
dependencies:
locate-path: "npm:^7.2.0"
path-exists: "npm:^5.0.0"
unicorn-magic: "npm:^0.1.0"
checksum: 7e6b08fbc05a10677e25e74bb0a020054a86b31d1806c5e6a9e32e75472bbf177210bc16e5f97453be8bda7ae2e3d97669dbb2901f8c30b39ce53929cbea6746
languageName: node
linkType: hard
"find-versions@npm:^5.0.0":
version: 5.1.0
resolution: "find-versions@npm:5.1.0"
@@ -2426,6 +2439,15 @@ __metadata:
languageName: node
linkType: hard
"locate-path@npm:^7.2.0":
version: 7.2.0
resolution: "locate-path@npm:7.2.0"
dependencies:
p-locate: "npm:^6.0.0"
checksum: 1c6d269d4efec555937081be964e8a9b4a136319c79ca1d45ac6382212a8466113c75bd89e44521ca8ecd1c47fb08523b56eee5c0712bc7d14fec5f729deeb42
languageName: node
linkType: hard
"lodash.debounce@npm:^4.0.8":
version: 4.0.8
resolution: "lodash.debounce@npm:4.0.8"
@@ -3046,6 +3068,15 @@ __metadata:
languageName: node
linkType: hard
"p-limit@npm:^4.0.0":
version: 4.0.0
resolution: "p-limit@npm:4.0.0"
dependencies:
yocto-queue: "npm:^1.0.0"
checksum: 01d9d70695187788f984226e16c903475ec6a947ee7b21948d6f597bed788e3112cc7ec2e171c1d37125057a5f45f3da21d8653e04a3a793589e12e9e80e756b
languageName: node
linkType: hard
"p-locate@npm:^3.0.0":
version: 3.0.0
resolution: "p-locate@npm:3.0.0"
@@ -3064,6 +3095,15 @@ __metadata:
languageName: node
linkType: hard
"p-locate@npm:^6.0.0":
version: 6.0.0
resolution: "p-locate@npm:6.0.0"
dependencies:
p-limit: "npm:^4.0.0"
checksum: 2bfe5234efa5e7a4e74b30a5479a193fdd9236f8f6b4d2f3f69e3d286d9a7d7ab0c118a2a50142efcf4e41625def635bd9332d6cbf9cc65d85eb0718c579ab38
languageName: node
linkType: hard
"p-map@npm:^4.0.0":
version: 4.0.0
resolution: "p-map@npm:4.0.0"
@@ -3094,6 +3134,13 @@ __metadata:
languageName: node
linkType: hard
"path-exists@npm:^5.0.0":
version: 5.0.0
resolution: "path-exists@npm:5.0.0"
checksum: 8ca842868cab09423994596eb2c5ec2a971c17d1a3cb36dbf060592c730c725cd524b9067d7d2a1e031fef9ba7bd2ac6dc5ec9fb92aa693265f7be3987045254
languageName: node
linkType: hard
"path-is-absolute@npm:^1.0.0":
version: 1.0.1
resolution: "path-is-absolute@npm:1.0.1"
@@ -3983,6 +4030,13 @@ __metadata:
languageName: node
linkType: hard
"unicorn-magic@npm:^0.1.0":
version: 0.1.0
resolution: "unicorn-magic@npm:0.1.0"
checksum: 9b4d0e9809807823dc91d0920a4a4c0cff2de3ebc54ee87ac1ee9bc75eafd609b09d1f14495e0173aef26e01118706196b6ab06a75fe0841028b3983a8af313f
languageName: node
linkType: hard
"unique-filename@npm:^2.0.0":
version: 2.0.1
resolution: "unique-filename@npm:2.0.1"
@@ -4276,3 +4330,10 @@ __metadata:
checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700
languageName: node
linkType: hard
"yocto-queue@npm:^1.0.0":
version: 1.1.1
resolution: "yocto-queue@npm:1.1.1"
checksum: f2e05b767ed3141e6372a80af9caa4715d60969227f38b1a4370d60bffe153c9c5b33a862905609afc9b375ec57cd40999810d20e5e10229a204e8bde7ef255c
languageName: node
linkType: hard