diff --git a/src/bot.ts b/src/bot.ts index 5f2c8d4a0..4a4d5dc8b 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -121,6 +121,9 @@ import { } from "./handlers/mod.ts"; import { DiscordenoInteraction, transformInteraction } from "./transformers/interaction.ts"; import { DiscordenoIntegration, transformIntegration } from "./transformers/integration.ts"; +import { transformApplication } from "./transformers/application.ts"; +import { transformTeam } from "./transformers/team.ts"; +import { DiscordenoInvite, transformInvite } from "./transformers/invite.ts"; export async function createBot(options: CreateBotOptions) { return { @@ -135,10 +138,17 @@ export async function createBot(options: CreateBotOptions) { constants: createBotConstants(), handlers: createBotGatewayHandlers({}), cache: { - forEach: function ( - type: "DELETE_MESSAGES_FROM_GUILD" | "DELETE_CHANNELS_FROM_GUILD" | "DELETE_GUILD_FROM_MEMBER", + execute: async function ( + type: + | "GUILD_MEMBER_CHUNK" + | "GUILD_MEMBER_COUNT_DECREMENT" + | "GUILD_MEMBER_COUNT_INCREMENT" + | "DELETE_MESSAGES_FROM_GUILD" + | "DELETE_CHANNELS_FROM_GUILD" + | "DELETE_GUILD_FROM_MEMBER", options: Record ) {}, + fetchAllMembersProcessingRequests: new Collection(), guilds: { get: async function (id: bigint): Promise { return {} as any as DiscordenoGuild; @@ -233,7 +243,12 @@ export function createEventHandlers(events: Partial): EventHandle dispatchRequirements: events.dispatchRequirements ?? ignore, integrationCreate: events.integrationCreate ?? ignore, integrationDelete: events.integrationDelete ?? ignore, + integrationUpdate: events.integrationUpdate ?? ignore, interactionCreate: events.interactionCreate ?? ignore, + inviteCreate: events.inviteCreate ?? ignore, + inviteDelete: events.inviteDelete ?? ignore, + guildMemberAdd: events.guildMemberAdd ?? ignore, + guildMemberRemove: events.guildMemberRemove ?? ignore, channelCreate: events.channelCreate ?? ignore, voiceChannelLeave: events.voiceChannelLeave ?? ignore, channelDelete: events.channelDelete ?? ignore, @@ -246,7 +261,6 @@ export function createEventHandlers(events: Partial): EventHandle guildCreate: events.guildCreate ?? ignore, guildDelete: events.guildDelete ?? ignore, guildUpdate: events.guildUpdate ?? ignore, - integrationsUpdate: events.integrationsUpdate ?? ignore, raw: events.raw ?? ignore, stageInstanceCreate: events.stageInstanceCreate ?? ignore, stageInstanceDelete: events.stageInstanceDelete ?? ignore, @@ -476,7 +490,10 @@ export interface Transformers { role: typeof transformRole; voiceState: typeof transformVoiceState; interaction: typeof transformInteraction; - integration: typeof transformIntegration, + integration: typeof transformIntegration; + invite: typeof transformInvite; + application: typeof transformApplication; + team: typeof transformTeam; } export function createTransformers(options: Partial) { @@ -490,6 +507,9 @@ export function createTransformers(options: Partial) { role: options.role || transformRole, voiceState: options.voiceState || transformVoiceState, integration: options.integration || transformIntegration, + invite: options.invite || transformInvite, + application: options.application || transformApplication, + team: options.team || transformTeam, }; } @@ -602,7 +622,19 @@ export interface EventHandlers { interactionCreate: (bot: Bot, interaction: DiscordenoInteraction) => any; integrationCreate: (bot: Bot, integration: DiscordenoIntegration) => any; integrationDelete: (bot: Bot, payload: { id: bigint; guildId: bigint; applicationId?: bigint }) => any; - integrationsUpdate: (bot: Bot, integration: DiscordenoIntegration) => any; + integrationUpdate: (bot: Bot, integration: DiscordenoIntegration) => any; + inviteCreate: (bot: Bot, invite: DiscordenoInvite) => any; + inviteDelete: ( + bot: Bot, + payload: { + channelId: bigint; + guildId?: bigint; + code: string; + } + ) => any; + guildMemberAdd: (bot: Bot, member: DiscordenoMember, user: DiscordenoUser) => any; + guildMemberRemove: (bot: Bot, user: DiscordenoUser, guildId: bigint) => any; + guildMemberUpdate: (bot: Bot, member: DiscordenoMember, user: DiscordenoUser) => any; channelCreate: (bot: Bot, channel: DiscordenoChannel) => any; dispatchRequirements: (bot: Bot, data: GatewayPayload, shardId: number) => any; voiceChannelLeave: (bot: Bot, voiceState: DiscordenoVoiceState, channel: DiscordenoChannel) => any; diff --git a/src/handlers/invites/INVITE_CREATE.ts b/src/handlers/invites/INVITE_CREATE.ts index 039ebc4c7..4ce57ca7b 100644 --- a/src/handlers/invites/INVITE_CREATE.ts +++ b/src/handlers/invites/INVITE_CREATE.ts @@ -1,7 +1,8 @@ -import { eventHandlers } from "../../bot.ts"; +import { Bot } from "../../bot.ts"; import type { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts"; import type { InviteCreate } from "../../types/invites/invite_create.ts"; +import { SnakeCasedPropertiesDeep } from "../../types/util.ts"; -export function handleInviteCreate(data: DiscordGatewayPayload) { - eventHandlers.inviteCreate?.(data.d as InviteCreate); +export function handleInviteCreate(bot: Bot, data: DiscordGatewayPayload) { + bot.events.inviteCreate(bot, bot.transformers.invite(bot, data.d as SnakeCasedPropertiesDeep)); } diff --git a/src/handlers/invites/INVITE_DELETE.ts b/src/handlers/invites/INVITE_DELETE.ts index b1f3be86b..45d45247c 100644 --- a/src/handlers/invites/INVITE_DELETE.ts +++ b/src/handlers/invites/INVITE_DELETE.ts @@ -1,7 +1,17 @@ -import { eventHandlers } from "../../bot.ts"; +import { Bot } from "../../bot.ts"; import type { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts"; import type { InviteDelete } from "../../types/invites/invite_delete.ts"; +import { SnakeCasedPropertiesDeep } from "../../types/util.ts"; -export function handleInviteDelete(data: DiscordGatewayPayload) { - eventHandlers.inviteDelete?.(data.d as InviteDelete); +export function handleInviteDelete(bot: Bot, data: DiscordGatewayPayload) { + const payload = data.d as SnakeCasedPropertiesDeep; + + bot.events.inviteDelete(bot, { + /** The channel of the invite */ + channelId: bot.transformers.snowflake(payload.channel_id), + /** The guild of the invite */ + guildId: payload.guild_id ? bot.transformers.snowflake(payload.guild_id) : undefined, + /** The unique invite code */ + code: payload.code, + }); } diff --git a/src/handlers/members/GUILD_MEMBERS_CHUNK.ts b/src/handlers/members/GUILD_MEMBERS_CHUNK.ts index 61a326e8f..b3271660f 100644 --- a/src/handlers/members/GUILD_MEMBERS_CHUNK.ts +++ b/src/handlers/members/GUILD_MEMBERS_CHUNK.ts @@ -1,37 +1,30 @@ -import { cache, cacheHandlers } from "../../cache.ts"; -import { structures } from "../../structures/mod.ts"; +import { Bot } from "../../bot.ts"; import type { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts"; import type { GuildMembersChunk } from "../../types/members/guild_members_chunk.ts"; -import { snowflakeToBigint } from "../../util/bigint.ts"; -import { Collection } from "../../util/collection.ts"; +import { SnakeCasedPropertiesDeep } from "../../types/util.ts"; -export async function handleGuildMembersChunk(data: DiscordGatewayPayload) { - const payload = data.d as GuildMembersChunk; +export async function handleGuildMembersChunk(bot: Bot, data: DiscordGatewayPayload) { + const payload = data.d as SnakeCasedPropertiesDeep; - const guildId = snowflakeToBigint(payload.guildId); + const guildId = bot.transformers.snowflake(payload.guild_id); - const members = await Promise.all( - payload.members.map(async (member) => { - const discordenoMember = await structures.createDiscordenoMember(member, guildId); - await cacheHandlers.set("members", discordenoMember.id, discordenoMember); - - return discordenoMember; - }) - ); + await bot.cache.execute("GUILD_MEMBER_CHUNK", { + members: payload.members.map((m) => bot.transformers.member(bot, m, guildId)), + users: payload.members.map((m) => bot.transformers.user(bot, m.user)), + }); + if (!payload.nonce) return; + // Check if its necessary to resolve the fetchmembers promise for this chunk or if more chunks will be coming - if (payload.nonce) { - const resolve = cache.fetchAllMembersProcessingRequests.get(payload.nonce); - if (!resolve) return; + const resolve = bot.cache.fetchAllMembersProcessingRequests.get(payload.nonce); + if (!resolve) return; - if (payload.chunkIndex + 1 === payload.chunkCount) { - cache.fetchAllMembersProcessingRequests.delete(payload.nonce); - // Only 1 chunk most likely is all members or users only request a small amount of users - if (payload.chunkCount === 1) { - return resolve(new Collection(members.map((m) => [m.id, m]))); - } - - return resolve(await cacheHandlers.filter("GET_MEMBERS_IN_GUILD", { guildId })); - } + if (payload.chunk_index + 1 === payload.chunk_count) { + bot.cache.fetchAllMembersProcessingRequests.delete(payload.nonce); + return resolve("Finished chunking members"); } } + +// TODO: add a helper function that runs await fetch +// await fetchMembers(); +// const members = await bot.cache.members.findMany(guildId); \ No newline at end of file diff --git a/src/handlers/members/GUILD_MEMBER_ADD.ts b/src/handlers/members/GUILD_MEMBER_ADD.ts index f75ad1555..2a8868a4c 100644 --- a/src/handlers/members/GUILD_MEMBER_ADD.ts +++ b/src/handlers/members/GUILD_MEMBER_ADD.ts @@ -1,18 +1,19 @@ -import { eventHandlers } from "../../bot.ts"; -import { cacheHandlers } from "../../cache.ts"; -import { structures } from "../../structures/mod.ts"; +import { Bot } from "../../bot.ts"; import type { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts"; import type { GuildMemberAdd } from "../../types/members/guild_member_add.ts"; -import { snowflakeToBigint } from "../../util/bigint.ts"; +import { SnakeCasedPropertiesDeep } from "../../types/util.ts"; -export async function handleGuildMemberAdd(data: DiscordGatewayPayload) { - const payload = data.d as GuildMemberAdd; - const guild = await cacheHandlers.get("guilds", snowflakeToBigint(payload.guildId)); - if (!guild) return; +export async function handleGuildMemberAdd(bot: Bot, data: DiscordGatewayPayload) { + const payload = data.d as SnakeCasedPropertiesDeep; + const guildId = bot.transformers.snowflake(payload.guild_id); + const member = bot.transformers.member(bot, payload, guildId); + const user = bot.transformers.user(bot, payload.user); - guild.memberCount++; - const discordenoMember = await structures.createDiscordenoMember(payload, guild.id); - await cacheHandlers.set("members", discordenoMember.id, discordenoMember); + await Promise.all([ + bot.cache.members.set(member.id, member), + bot.cache.users.set(user.id, user), + bot.cache.execute("GUILD_MEMBER_COUNT_INCREMENT", { guildId }), + ]); - eventHandlers.guildMemberAdd?.(guild, discordenoMember); + bot.events.guildMemberAdd(bot, member, user); } diff --git a/src/handlers/members/GUILD_MEMBER_REMOVE.ts b/src/handlers/members/GUILD_MEMBER_REMOVE.ts index 98cacb5ab..1ab13e320 100644 --- a/src/handlers/members/GUILD_MEMBER_REMOVE.ts +++ b/src/handlers/members/GUILD_MEMBER_REMOVE.ts @@ -1,20 +1,18 @@ -import { eventHandlers } from "../../bot.ts"; -import { cacheHandlers } from "../../cache.ts"; +import { Bot } from "../../bot.ts"; import type { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts"; import type { GuildMemberRemove } from "../../types/members/guild_member_remove.ts"; +import { SnakeCasedPropertiesDeep } from "../../types/util.ts"; import { snowflakeToBigint } from "../../util/bigint.ts"; -export async function handleGuildMemberRemove(data: DiscordGatewayPayload) { - const payload = data.d as GuildMemberRemove; - const guild = await cacheHandlers.get("guilds", snowflakeToBigint(payload.guildId)); - if (!guild) return; +export async function handleGuildMemberRemove(bot: Bot, data: DiscordGatewayPayload) { + const payload = data.d as SnakeCasedPropertiesDeep; + const guildId = bot.transformers.snowflake(payload.guild_id); + const user = bot.transformers.user(bot, payload.user); - guild.memberCount--; - const member = await cacheHandlers.get("members", snowflakeToBigint(payload.user.id)); - eventHandlers.guildMemberRemove?.(guild, payload.user, member); + await Promise.all([ + bot.cache.members.delete(user.id), + bot.cache.execute("GUILD_MEMBER_COUNT_DECREMENT", { guildId }), + ]); - member?.guilds.delete(guild.id); - if (member && !member.guilds.size) { - await cacheHandlers.delete("members", member.id); - } + bot.events.guildMemberRemove(bot, user, guildId); } diff --git a/src/handlers/members/GUILD_MEMBER_UPDATE.ts b/src/handlers/members/GUILD_MEMBER_UPDATE.ts index 4f8ca7fe4..0fde4ceb8 100644 --- a/src/handlers/members/GUILD_MEMBER_UPDATE.ts +++ b/src/handlers/members/GUILD_MEMBER_UPDATE.ts @@ -1,54 +1,17 @@ -import { eventHandlers } from "../../bot.ts"; -import { cacheHandlers } from "../../cache.ts"; -import { structures } from "../../structures/mod.ts"; +import { Bot } from "../../bot.ts"; import type { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts"; import type { GuildMemberUpdate } from "../../types/members/guild_member_update.ts"; -import { bigintToSnowflake, snowflakeToBigint } from "../../util/bigint.ts"; +import { SnakeCasedPropertiesDeep } from "../../types/util.ts"; -export async function handleGuildMemberUpdate(data: DiscordGatewayPayload) { - const payload = data.d as GuildMemberUpdate; - const guild = await cacheHandlers.get("guilds", snowflakeToBigint(payload.guildId)); - if (!guild) return; +export async function handleGuildMemberUpdate(bot: Bot, data: DiscordGatewayPayload) { + const payload = data.d as SnakeCasedPropertiesDeep; - const cachedMember = await cacheHandlers.get("members", snowflakeToBigint(payload.user.id)); - const guildMember = cachedMember?.guilds.get(guild.id); + // TODO: IDK IF THIS IS BUT IS IT LURKER IN STAGE CHANNEL WHO ISN'T A MEMBER + if (!payload.joined_at) return; - const newMemberData = { - ...payload, - premiumSince: payload.premiumSince || undefined, - joinedAt: new Date(guildMember?.joinedAt || Date.now()).toISOString(), - deaf: guildMember?.deaf || false, - mute: guildMember?.mute || false, - roles: payload.roles, - }; - const discordenoMember = await structures.createDiscordenoMember(newMemberData, guild.id); - await cacheHandlers.set("members", discordenoMember.id, discordenoMember); - - if (guildMember) { - if (guildMember.nick !== payload.nick) { - eventHandlers.nicknameUpdate?.(guild, discordenoMember, payload.nick!, guildMember.nick ?? undefined); - } - - if (payload.pending === false && guildMember.pending === true) { - eventHandlers.membershipScreeningPassed?.(guild, discordenoMember); - } - - const roleIds = guildMember.roles || []; - - roleIds.forEach((id) => { - eventHandlers.debug?.("loop", `1. Running forEach loop in GUILD_MEMBER_UPDATE file.`); - if (!payload.roles.includes(bigintToSnowflake(id))) { - eventHandlers.roleLost?.(guild, discordenoMember, id); - } - }); - - payload.roles.forEach((id) => { - eventHandlers.debug?.("loop", `2. Running forEach loop in GUILD_MEMBER_UPDATE file.`); - if (!roleIds.includes(snowflakeToBigint(id))) { - eventHandlers.roleGained?.(guild, discordenoMember, snowflakeToBigint(id)); - } - }); - } - - eventHandlers.guildMemberUpdate?.(guild, discordenoMember, cachedMember); + bot.events.guildMemberUpdate( + bot, + bot.transformers.member(bot, payload, bot.transformers.snowflake(payload.guild_id)), + bot.transformers.user(bot, payload.user) + ); } diff --git a/src/transformers/application.ts b/src/transformers/application.ts new file mode 100644 index 000000000..b2c30a85c --- /dev/null +++ b/src/transformers/application.ts @@ -0,0 +1,69 @@ +import { Bot } from "../bot.ts"; +import { Application } from "../types/applications/application.ts"; +import { DiscordApplicationFlags } from "../types/applications/application_flags.ts"; +import { SnakeCasedPropertiesDeep } from "../types/util.ts"; +import { DiscordenoUser } from "./member.ts"; +import { DiscordenoTeam } from "./team.ts"; + +export function transformApplication(bot: Bot, payload: SnakeCasedPropertiesDeep): DiscordenoApplication { + return { + name: payload.name, + description: payload.description, + rpcOrigins: payload.rpc_origins, + botPublic: payload.bot_public, + botRequireCodeGrant: payload.bot_require_code_grant, + termsOfServiceUrl: payload.terms_of_service_url, + privacyPolicyUrl: payload.privacy_policy_url, + summary: payload.summary, + verifyKey: payload.verify_key, + primarySkuId: payload.primary_sku_id, + slug: payload.slug, + coverImage: payload.cover_image, + flags: payload.flags, + + id: bot.transformers.snowflake(payload.id), + icon: payload.icon ? bot.utils.iconHashToBigInt(payload.icon) : undefined, + owner: payload.owner ? bot.transformers.user(bot, payload.owner) : undefined, + team: payload.team ? bot.transformers.team(bot, payload.team) : undefined, + guildId: payload.guild_id ? bot.transformers.snowflake(payload.guild_id) : undefined, + }; +} + +export interface DiscordenoApplication { + /** The id of the app */ + id: bigint; + /** The name of the app */ + name: string; + /** The icon hash of the app */ + icon?: bigint; + /** The description of the app */ + description: string; + /** An array of rpc origin urls, if rpc is enabled */ + rpcOrigins?: string[]; + /** When false only app owner can join the app's bot to guilds */ + botPublic: boolean; + /** When true the app's bot will only join upon completion of the full oauth2 code grant flow */ + botRequireCodeGrant: boolean; + /** The url of the app's terms of service */ + termsOfServiceUrl?: string; + /** The url of the app's privacy policy */ + privacyPolicyUrl?: string; + /** Partial user object containing info on the owner of the application */ + owner?: Partial; + /** If this application is a game sold on Discord, this field will be the summary field for the store page of its primary sku */ + summary: string; + /** The hex encoded key for verification in interactions and the GameSDK's GetTicket */ + verifyKey: string; + /** If the application belongs to a team, this will be a list of the members of that team */ + team?: DiscordenoTeam; + /** If this application is a game sold on Discord, this field will be the guild to which it has been linked */ + guildId?: bigint; + /** If this application is a game sold on Discord, this field will be the id of the "Game SKU" that is created, if exists */ + primarySkuId?: string; + /** If this application is a game sold on Discord, this field will be the URL slug that links to the store page */ + slug?: string; + /** If this application is a game sold on Discord, this field will be the hash of the image on store embeds */ + coverImage?: string; + /** The application's public flags */ + flags: DiscordApplicationFlags; +} diff --git a/src/transformers/invite.ts b/src/transformers/invite.ts new file mode 100644 index 000000000..74aa4bd59 --- /dev/null +++ b/src/transformers/invite.ts @@ -0,0 +1,63 @@ +import { Bot } from "../bot.ts"; +import { InviteCreate, DiscordTargetTypes, User, Application } from "../types/mod.ts"; +import { SnakeCasedPropertiesDeep } from "../types/util.ts"; +import { DiscordenoApplication } from "./application.ts"; +import { DiscordenoUser } from "./member.ts"; + +export function transformInvite(bot: Bot, invite: SnakeCasedPropertiesDeep): DiscordenoInvite { + return { + /** The channel the invite is for */ + channelId: bot.transformers.snowflake(invite.channel_id), + /** The unique invite code */ + code: invite.code, + /** The time at which the invite was created */ + createdAt: Date.parse(invite.created_at), + /** The guild of the invite */ + guildId: invite.guild_id ? bot.transformers.snowflake(invite.guild_id) : undefined, + /** The user that created the invite */ + inviter: invite.inviter ? bot.transformers.user(bot, invite.inviter) : undefined, + /** How long the invite is valid for (in seconds) */ + maxAge: invite.max_age, + /** The maximum number of times the invite can be used */ + maxUses: invite.max_uses, + /** The type of target for this voice channel invite */ + targetType: invite.target_type, + /** The target user for this invite */ + targetUser: invite.target_user ? bot.transformers.user(bot, invite.target_user) : undefined, + /** The embedded application to open for this voice channel embedded application invite */ + targetApplication: invite.target_application + ? bot.transformers.application(bot, invite.target_application) + : undefined, + /** Whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role) */ + temporary: invite.temporary, + /** How many times the invite has been used (always will be 0) */ + uses: invite.uses, + }; +} + +export interface DiscordenoInvite { + /** 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?: DiscordenoUser; + /** 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: DiscordTargetTypes; + /** The target user for this invite */ + targetUser?: DiscordenoUser; + /** The embedded application to open for this voice channel embedded application invite */ + targetApplication?: Partial; + /** 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; +} diff --git a/src/transformers/team.ts b/src/transformers/team.ts new file mode 100644 index 000000000..05c8a74d0 --- /dev/null +++ b/src/transformers/team.ts @@ -0,0 +1,47 @@ +import { Bot } from "../bot.ts"; +import { Team } from "../types/teams/team.ts"; +import { DiscordTeamMembershipStates } from "../types/teams/team_membership_states.ts"; +import { SnakeCasedPropertiesDeep } from "../types/util.ts"; +import { DiscordenoUser } from "./member.ts"; + +export function transformTeam(bot: Bot, payload: SnakeCasedPropertiesDeep): DiscordenoTeam { + const id = bot.transformers.snowflake(payload.id); + + return { + name: payload.name, + + id, + icon: payload.icon ? bot.utils.iconHashToBigInt(payload.icon) : undefined, + ownerUserId: bot.transformers.snowflake(payload.owner_user_id), + members: payload.members.map((member) => ({ + membershipState: member.membership_state, + // TODO: think about this seems useless to add ["*"] to everything + permissions: member.permissions, + // TODO: think about this seems useless to add another id here when its also on the one above + teamId: id, + user: bot.transformers.user(bot, member.user), + })), + }; +} + +export interface DiscordenoTeam { + /** A hash of the image of the team's icon */ + icon?: bigint; + /** The unique id of the team */ + id: bigint; + /** The members of the team */ + members: { + /** The user's membership state on the team */ + membershipState: DiscordTeamMembershipStates; + /** Will always be `["*"]` */ + permissions: "*"[]; + /** The id of the parent team of which they are a member */ + teamId: bigint; + /** The avatar, discriminator, id, and username of the user */ + user: Partial; + }[]; + /** The name of the team */ + name: string; + /** The user id of the current team owner */ + ownerUserId: bigint; +} diff --git a/src/types/invites/invite_create.ts b/src/types/invites/invite_create.ts index 15b3f17b8..672e1835d 100644 --- a/src/types/invites/invite_create.ts +++ b/src/types/invites/invite_create.ts @@ -21,7 +21,7 @@ export interface InviteCreate { /** The type of target for this voice channel invite */ targetType: DiscordTargetTypes; /** The target user for this invite */ - targetUser?: Partial; + targetUser?: User; /** The embedded application to open for this voice channel embedded application invite */ targetApplication?: Partial; /** Whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role) */