From cf024810869e1b47bc8af1b39b5e437a785b6733 Mon Sep 17 00:00:00 2001 From: Fleny Date: Wed, 28 Jan 2026 05:43:35 +0100 Subject: [PATCH] refactor(bot)!: setup desired properties for all transformers (#4435) * refactor(bot)!: setup desired properties for all transformers SetupDesiredProps when is given an object that does not corrispond to a transformer object that supports desired properties will behave like TransformProperty on the entire object as when it tries to get the properties for said object it will find `never` as the props and for `IsKeyDesired` a props of `never` means that all props are desired. * Use Equals helper, clean up a bit the code * Explicit the IsKeyDesired TProps never behavior * Add all trasformer objects to bot.transformers.$inferredTypes --- packages/bot/src/bot.ts | 12 +- packages/bot/src/desiredProperties.ts | 300 +++++++++--------- packages/bot/src/events.ts | 25 +- .../channels/THREAD_MEMBERS_UPDATE.ts | 2 +- packages/bot/src/helpers.ts | 125 +++++--- packages/bot/src/transformers.ts | 114 ++----- packages/bot/src/transformers/component.ts | 57 ++-- packages/bot/src/transformers/presence.ts | 3 +- packages/bot/src/transformers/webhook.ts | 6 +- 9 files changed, 321 insertions(+), 323 deletions(-) diff --git a/packages/bot/src/bot.ts b/packages/bot/src/bot.ts index 46feedb6c..756f36724 100644 --- a/packages/bot/src/bot.ts +++ b/packages/bot/src/bot.ts @@ -4,17 +4,11 @@ import type { CreateRestManagerOptions, RestManager } from '@discordeno/rest'; import { createRestManager } from '@discordeno/rest'; import type { BigString, GatewayDispatchEventNames, GatewayIntents, RecursivePartial } from '@discordeno/types'; import { createLogger, getBotIdFromToken, type logger } from '@discordeno/utils'; -import type { - CompleteDesiredProperties, - DesiredPropertiesBehavior, - SetupDesiredProps, - TransformersDesiredProperties, - TransformersObjects, -} from './desiredProperties.js'; +import type { CompleteDesiredProperties, DesiredPropertiesBehavior, TransformersDesiredProperties } from './desiredProperties.js'; import type { EventHandlers } from './events.js'; import { type BotGatewayHandler, createBotGatewayHandlers, type GatewayHandlers } from './handlers.js'; import { type BotHelpers, createBotHelpers } from './helpers.js'; -import { createTransformers, type Transformers } from './transformers.js'; +import { createTransformers, type TransformerFunctions, type Transformers } from './transformers.js'; /** * Create a bot object that will maintain the rest and gateway connection. @@ -166,7 +160,7 @@ export interface Bot< /** The functions that should transform discord objects to discordeno shaped objects. */ transformers: Transformers & { $inferredTypes: { - [K in keyof TransformersObjects]: SetupDesiredProps; + [K in keyof TransformerFunctions]: ReturnType[K]>; }; }; /** The handler functions that should handle incoming discord payloads from gateway and call an event. */ diff --git a/packages/bot/src/desiredProperties.ts b/packages/bot/src/desiredProperties.ts index 501441dc9..644b3c90a 100644 --- a/packages/bot/src/desiredProperties.ts +++ b/packages/bot/src/desiredProperties.ts @@ -119,120 +119,128 @@ export interface TransformersObjects { webhook: Webhook; } -// NOTE: the top-level objects need both the dependencies and alwaysPresents even if empty when the key is specified, this is due the extends & nullability on DesiredPropertiesMetadata -// internal properties needs to be in the alwaysPresents array, depending on an always present value is accepted +/** + * Metadata for typescript to create the correct types for desired properties + * + * @private This is subject to breaking changes without notices + */ +export const transformersDesiredPropertiesMetadata = { + channel: { + dependencies: { + archived: ['toggles'], + invitable: ['toggles'], + locked: ['toggles'], + nsfw: ['toggles'], + newlyCreated: ['toggles'], + managed: ['toggles'], + }, + alwaysPresents: ['toggles', 'internalOverwrites', 'internalThreadMetadata'], + }, + + guild: { + dependencies: { + threads: ['channels'], + features: ['toggles'], + }, + alwaysPresents: [], + }, + + interaction: { + dependencies: { + respond: ['type', 'token', 'id'], + sendFollowupMessage: ['token'], + edit: ['type', 'token', 'id'], + deferEdit: ['type', 'token', 'id'], + defer: ['type', 'token', 'id'], + delete: ['type', 'token'], + }, + alwaysPresents: ['bot', 'acknowledged'], + }, + + member: { + dependencies: { + deaf: ['toggles'], + mute: ['toggles'], + pending: ['toggles'], + flags: ['toggles'], + didRejoin: ['toggles'], + startedOnboarding: ['toggles'], + bypassesVerification: ['toggles'], + completedOnboarding: ['toggles'], + }, + alwaysPresents: [], + }, + + message: { + dependencies: { + 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'], + }, + alwaysPresents: ['bitfield', 'flags'], + }, + + role: { + dependencies: { + hoist: ['toggles'], + managed: ['toggles'], + mentionable: ['toggles'], + premiumSubscriber: ['toggles'], + availableForPurchase: ['toggles'], + guildConnections: ['toggles'], + }, + alwaysPresents: ['internalTags'], + }, + + user: { + dependencies: { + tag: ['username', 'discriminator'], + bot: ['toggles'], + system: ['toggles'], + mfaEnabled: ['toggles'], + verified: ['toggles'], + avatarUrl: ['avatar', 'id'], + displayName: ['username', 'globalName'], + defaultAvatarUrl: ['id', 'discriminator'], + displayAvatarUrl: ['avatar', 'id', 'discriminator'], + createdTimestamp: ['id'], + }, + alwaysPresents: [], + }, + + emoji: { + dependencies: { + animated: ['toggles'], + available: ['toggles'], + managed: ['toggles'], + requireColons: ['toggles'], + }, + alwaysPresents: ['toggles'], + }, +} as const satisfies DesiredPropertiesMetadata; /** * Metadata for typescript to create the correct types for desired properties * * @private This is subject to breaking changes without notices */ -export interface TransformersDesiredPropertiesMetadata extends DesiredPropertiesMetadata { - channel: { - dependencies: { - archived: ['toggles']; - invitable: ['toggles']; - locked: ['toggles']; - nsfw: ['toggles']; - newlyCreated: ['toggles']; - managed: ['toggles']; - }; - alwaysPresents: ['toggles', 'internalOverwrites', 'internalThreadMetadata']; - }; - - guild: { - dependencies: { - threads: ['channels']; - features: ['toggles']; - }; - alwaysPresents: []; - }; - - interaction: { - dependencies: { - respond: ['type', 'token', 'id']; - sendFollowupMessage: ['token']; - edit: ['type', 'token', 'id']; - deferEdit: ['type', 'token', 'id']; - defer: ['type', 'token', 'id']; - delete: ['type', 'token']; - }; - alwaysPresents: ['bot', 'acknowledged']; - }; - - member: { - dependencies: { - deaf: ['toggles']; - mute: ['toggles']; - pending: ['toggles']; - flags: ['toggles']; - didRejoin: ['toggles']; - startedOnboarding: ['toggles']; - bypassesVerification: ['toggles']; - completedOnboarding: ['toggles']; - }; - alwaysPresents: []; - }; - - message: { - dependencies: { - 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']; - }; - alwaysPresents: ['bitfield', 'flags']; - }; - - role: { - dependencies: { - hoist: ['toggles']; - managed: ['toggles']; - mentionable: ['toggles']; - premiumSubscriber: ['toggles']; - availableForPurchase: ['toggles']; - guildConnections: ['toggles']; - }; - alwaysPresents: ['internalTags']; - }; - - user: { - dependencies: { - tag: ['username', 'discriminator']; - bot: ['toggles']; - system: ['toggles']; - mfaEnabled: ['toggles']; - verified: ['toggles']; - avatarUrl: ['avatar', 'id']; - displayName: ['username', 'globalName']; - defaultAvatarUrl: ['id', 'discriminator']; - displayAvatarUrl: ['avatar', 'id', 'discriminator']; - createdTimestamp: ['id']; - }; - alwaysPresents: []; - }; - - emoji: { - dependencies: { - animated: ['toggles']; - available: ['toggles']; - managed: ['toggles']; - requireColons: ['toggles']; - }; - alwaysPresents: ['toggles']; - }; -} +type TransformersDesiredPropertiesMetadata = CompleteByKeys< + typeof transformersDesiredPropertiesMetadata, + keyof DesiredPropertiesMetadata, + { dependencies: {}; alwaysPresents: [] } +>; export function createDesiredPropertiesObject, TDefault extends boolean = false>( desiredProperties: T, @@ -864,9 +872,12 @@ export function createDesiredPropertiesObject; } +/** @private This is subject to breaking changes without notices */ +export type Equals = Required extends Required ? (Required extends Required ? true : false) : false; + /** @private This is subject to breaking changes without notices */ export type KeyByValue = { - [Key in keyof TObj]: TObj[Key] extends TValue ? Key : never; + [Key in keyof TObj]: Equals extends true ? Key : never; }[keyof TObj]; /** @private This is subject to breaking changes without notices */ @@ -874,6 +885,11 @@ export type Complete = { [K in keyof TObj]-?: undefined extends TObj[K] ? TDefault : Exclude; }; +/** @private This is subject to breaking changes without notices */ +export type CompleteByKeys = { + [K in Keys]-?: K extends keyof TObj ? (undefined extends TObj[K] ? TDefault : Exclude) : TDefault; +}; + /** @private This is subject to breaking changes without notices */ export type JoinTuple = T extends readonly [infer F extends string, ...infer R extends string[]] ? R['length'] extends 0 @@ -883,26 +899,21 @@ export type JoinTuple = T extends /** @private This is subject to breaking changes without notices */ export type DesiredPropertiesMetadata = { - [K in keyof TransformersObjects]: { - dependencies?: { + [K in keyof TransformersObjects]?: { + dependencies: { [Key in keyof TransformersObjects[K]]?: (keyof TransformersObjects[K])[]; }; - alwaysPresents?: (keyof TransformersObjects[K])[]; + alwaysPresents: (keyof TransformersObjects[K])[]; }; }; /** @private This is subject to breaking changes without notices */ -export type DesirableProperties< - T extends TransformersObjects[keyof TransformersObjects], - TKey extends keyof TransformersObjects = KeyByValue, -> = Exclude< +export type DesirableProperties = Exclude< keyof T, // Exclude the props that depend on something else from the desirable properties - | keyof TransformersDesiredPropertiesMetadata[TKey]['dependencies'] - // Check if all the keys are "always presents", if this is the case it means we did not specify any always present key - | (keyof T extends NonNullable[number] - ? never - : NonNullable[number]) + | keyof TransformersDesiredPropertiesMetadata[KeyByValue]['dependencies'] + // Exclude the props that are always present + | TransformersDesiredPropertiesMetadata[KeyByValue]['alwaysPresents'][number] >; /** @private This is subject to breaking changes without notices */ @@ -923,23 +934,25 @@ export type AreDependenciesSatisfied | undefined, TProps> = TKey extends keyof TProps // The key has a desired props? - ? // Yes, is it true? - TProps[TKey] extends true - ? // Yes, this is a key to include - true - : // No, this is a key to exclude - DesiredPropertiesError<`This property is not set as desired in desiredProperties option in createBot(), so you can't use it. More info here: https://discordeno.js.org/desired-props`> - : // No, it is a props with dependencies? - TKey extends keyof TDependencies - ? // Yes, has all of its dependencies satisfied? - AreDependenciesSatisfied extends true[] +export type IsKeyDesired | undefined, TProps> = TProps extends never // If the props are never, all props are allowed + ? true + : TKey extends keyof TProps // The key has a desired props? + ? // Yes, is it true? + TProps[TKey] extends true ? // Yes, this is a key to include true - : // No, this is a key to not include - DesiredPropertiesError<`This property depends on the following properties: ${JoinTuple[TKey], ', '>}. Not all of these props are set as desired in desiredProperties option in createBot(), so you can't use it. More info here: https://discordeno.js.org/desired-props`> - : // No, we include it but it does not have neither props nor dependencies - true; + : // No, this is a key to exclude + DesiredPropertiesError<`This property is not set as desired in desiredProperties option in createBot(), so you can't use it. More info here: https://discordeno.js.org/desired-props`> + : // No, it is a props with dependencies? + TKey extends keyof TDependencies + ? // Yes, has all of its dependencies satisfied? + AreDependenciesSatisfied extends true[] + ? // Yes, this is a key to include + true + : // No, this is a key to not include + DesiredPropertiesError<`This property depends on the following properties: ${JoinTuple[TKey], ', '>}. Not all of these props are set as desired in desiredProperties option in createBot(), so you can't use it. More info here: https://discordeno.js.org/desired-props`> + : // No, we include it but it does not have neither props nor dependencies + true; /** The behavior it should be used when resolving an undesired property */ export enum DesiredPropertiesBehavior { @@ -997,7 +1010,7 @@ export type TransformProperty> : // No, is it a Bot? - T extends Bot + Equals extends true ? // Yes, return a bot with the correct set of props & behavior Bot : // No, is it a transformed object? @@ -1005,25 +1018,28 @@ export type TransformProperty : // No, is it an interaction resolved data member? | We need to check this here because the type itself has not way of getting the desired props - T extends InteractionResolvedDataMember + Equals> extends true ? // Yes, apply the desired props InteractionResolvedDataMember : // No, is it an interaction resolved data channel? | We need to check this here because the type itself has not way of getting the desired props - T extends InteractionResolvedDataChannel + Equals> extends true ? // Yes, apply the desired props InteractionResolvedDataChannel : // Is it an object? IsObject extends true - ? // Yes, we need to ensure nested inside there aren't transformed objects + ? // Yes, we need to ensure we transform the nested properties as well { [K in keyof T]: TransformProperty } : // No, this is a normal value such as string / bigint / number T; /** - * Apply desired properties to a transformer object. + * Apply desired properties to an object. + * + * @remarks + * If the object is not a transformed object that supports desired properties this function behaves the same as TransformProperty on the entire object */ export type SetupDesiredProps< - T extends TransformersObjects[keyof TransformersObjects], + T, TProps extends TransformersDesiredProperties, TBehavior extends DesiredPropertiesBehavior = DesiredPropertiesBehavior.RemoveKey, > = { diff --git a/packages/bot/src/events.ts b/packages/bot/src/events.ts index fc397408d..f22a42493 100644 --- a/packages/bot/src/events.ts +++ b/packages/bot/src/events.ts @@ -27,22 +27,27 @@ import type { } from './transformers/types.js'; export type EventHandlers = { - applicationCommandPermissionsUpdate: (command: GuildApplicationCommandPermissions) => unknown; - guildAuditLogEntryCreate: (log: AuditLogEntry, guildId: bigint) => unknown; - automodRuleCreate: (rule: AutoModerationRule) => unknown; - automodRuleUpdate: (rule: AutoModerationRule) => unknown; - automodRuleDelete: (rule: AutoModerationRule) => unknown; - automodActionExecution: (payload: AutoModerationActionExecution) => unknown; + applicationCommandPermissionsUpdate: (command: SetupDesiredProps) => unknown; + guildAuditLogEntryCreate: (log: SetupDesiredProps, guildId: bigint) => unknown; + automodRuleCreate: (rule: SetupDesiredProps) => unknown; + automodRuleUpdate: (rule: SetupDesiredProps) => unknown; + automodRuleDelete: (rule: SetupDesiredProps) => unknown; + automodActionExecution: (payload: SetupDesiredProps) => unknown; threadCreate: (thread: SetupDesiredProps) => unknown; threadDelete: (thread: SetupDesiredProps) => unknown; threadListSync: (payload: { guildId: bigint; channelIds?: bigint[]; threads: SetupDesiredProps[]; - members: ThreadMember[]; + members: SetupDesiredProps[]; }) => unknown; threadMemberUpdate: (payload: { id: bigint; guildId: bigint; joinedTimestamp: number; flags: number }) => unknown; - threadMembersUpdate: (payload: { id: bigint; guildId: bigint; addedMembers?: ThreadMember[]; removedMemberIds?: bigint[] }) => unknown; + threadMembersUpdate: (payload: { + id: bigint; + guildId: bigint; + addedMembers?: SetupDesiredProps[]; + removedMemberIds?: bigint[]; + }) => unknown; threadUpdate: (thread: SetupDesiredProps) => unknown; scheduledEventCreate: (event: SetupDesiredProps) => unknown; scheduledEventUpdate: (event: SetupDesiredProps) => unknown; @@ -64,7 +69,7 @@ export type EventHandlers unknown; rateLimited: (data: DiscordRateLimited, shardId: number) => unknown; interactionCreate: (interaction: SetupDesiredProps) => unknown; - integrationCreate: (integration: Integration) => unknown; + integrationCreate: (integration: SetupDesiredProps) => unknown; integrationDelete: (payload: { id: bigint; guildId: bigint; applicationId?: bigint }) => unknown; integrationUpdate: (payload: { guildId: bigint }) => unknown; inviteCreate: (invite: SetupDesiredProps) => unknown; @@ -104,7 +109,7 @@ export type EventHandlers; }) => unknown; reactionRemoveAll: (payload: { channelId: bigint; messageId: bigint; guildId?: bigint }) => unknown; - presenceUpdate: (presence: PresenceUpdate) => unknown; + presenceUpdate: (presence: SetupDesiredProps) => unknown; voiceChannelEffectSend: (payload: { channelId: bigint; guildId: bigint; diff --git a/packages/bot/src/handlers/channels/THREAD_MEMBERS_UPDATE.ts b/packages/bot/src/handlers/channels/THREAD_MEMBERS_UPDATE.ts index 8e81f468e..8d34b735d 100644 --- a/packages/bot/src/handlers/channels/THREAD_MEMBERS_UPDATE.ts +++ b/packages/bot/src/handlers/channels/THREAD_MEMBERS_UPDATE.ts @@ -9,7 +9,7 @@ export async function handleThreadMembersUpdate(bot: Bot, data: DiscordGatewayPa bot.events.threadMembersUpdate({ id: bot.transformers.snowflake(payload.id), guildId: bot.transformers.snowflake(payload.guild_id), - addedMembers: payload.added_members?.map((member) => bot.transformers.threadMember?.(bot, member, { guildId: payload.guild_id })), + addedMembers: payload.added_members?.map((member) => bot.transformers.threadMember(bot, member, { guildId: payload.guild_id })), removedMemberIds: payload.removed_member_ids?.map((id) => bot.transformers.snowflake(id)), }); } diff --git a/packages/bot/src/helpers.ts b/packages/bot/src/helpers.ts index 06332727f..7ad025d34 100644 --- a/packages/bot/src/helpers.ts +++ b/packages/bot/src/helpers.ts @@ -39,18 +39,15 @@ import type { DiscordConnection, DiscordCurrentAuthorization, DiscordFollowedChannel, - DiscordGetGatewayBot, DiscordGuildPreview, DiscordGuildWidgetSettings, DiscordInvite, - DiscordInviteMetadata, DiscordListArchivedThreads, DiscordPrunedCount, DiscordTargetUsersJobStatus, DiscordTokenExchange, DiscordTokenRevocation, DiscordVanityUrl, - DiscordVoiceRegion, EditApplication, EditAutoModerationRuleOptions, EditBotMemberOptions, @@ -118,6 +115,7 @@ import type { Channel, Emoji, Entitlement, + GetGatewayBot, Guild, GuildApplicationCommandPermissions, GuildOnboarding, @@ -142,6 +140,7 @@ import type { Template, ThreadMember, User, + VoiceRegion, VoiceState, Webhook, WelcomeScreen, @@ -332,8 +331,7 @@ export function createBotHelpers { - return await bot.rest.getChannelInvites(channelId); - // return (await bot.rest.getChannelInvites(channelId)).map((res) => bot.transformers.invite(bot, snakelize(res))) + return (await bot.rest.getChannelInvites(channelId)).map((res) => bot.transformers.invite(bot, snakelize(res))); }, getChannels: async (guildId) => { return (await bot.rest.getChannels(guildId)).map((res) => bot.transformers.channel(bot, snakelize(res), { guildId })); @@ -859,7 +857,11 @@ export function createBotHelpers = { - createAutomodRule: (guildId: BigString, options: CreateAutoModerationRuleOptions, reason?: string) => Promise; + createAutomodRule: ( + guildId: BigString, + options: CreateAutoModerationRuleOptions, + reason?: string, + ) => Promise>; createChannel: (guildId: BigString, options: CreateGuildChannel, reason?: string) => Promise>; createEmoji: (guildId: BigString, options: CreateGuildEmoji, reason?: string) => Promise>; createApplicationEmoji: (options: CreateApplicationEmoji) => Promise>; @@ -868,18 +870,21 @@ export type BotHelpers Promise>; - createGlobalApplicationCommand: (command: CreateApplicationCommand, options?: CreateGlobalApplicationCommandOptions) => Promise; + createGlobalApplicationCommand: ( + command: CreateApplicationCommand, + options?: CreateGlobalApplicationCommandOptions, + ) => Promise>; createGuildApplicationCommand: ( command: CreateApplicationCommand, guildId: BigString, options?: CreateGuildApplicationCommandOptions, - ) => Promise; + ) => Promise>; createGuildSticker: ( guildId: BigString, options: CreateGuildStickerOptions, reason?: string, ) => Promise>; - createGuildTemplate: (guildId: BigString, options: CreateTemplate) => Promise