diff --git a/.changeset/neat-ads-melt.md b/.changeset/neat-ads-melt.md new file mode 100644 index 00000000..52471bec --- /dev/null +++ b/.changeset/neat-ads-melt.md @@ -0,0 +1,5 @@ +--- +"@buape/carbon": patch +--- + +Add GuildMember structure diff --git a/packages/carbon/src/abstracts/BaseInteraction.ts b/packages/carbon/src/abstracts/BaseInteraction.ts index 88c23fd0..7b7a0519 100644 --- a/packages/carbon/src/abstracts/BaseInteraction.ts +++ b/packages/carbon/src/abstracts/BaseInteraction.ts @@ -9,6 +9,7 @@ import type { Client } from "../classes/Client.js" import type { Row } from "../classes/Row.js" import { channelFactory } from "../factories/channelFactory.js" import { Guild } from "../structures/Guild.js" +import { GuildMember } from "../structures/GuildMember.js" import { Message } from "../structures/Message.js" import { User } from "../structures/User.js" import { Base } from "./Base.js" @@ -109,6 +110,12 @@ export abstract class BaseInteraction extends Base { return channelFactory(this.client, this.rawData.channel as APIChannel) } + get member() { + if (!this.rawData.member) return null + if (!this.guild) return null + return new GuildMember(this.client, this.rawData.member, this.guild) + } + /** * Reply to an interaction. * If the interaction is deferred, this will edit the original response. diff --git a/packages/carbon/src/classes/Command.ts b/packages/carbon/src/classes/Command.ts index f32cb2a7..27007189 100644 --- a/packages/carbon/src/classes/Command.ts +++ b/packages/carbon/src/classes/Command.ts @@ -3,8 +3,8 @@ import { ApplicationCommandType } from "discord-api-types/v10" import { BaseCommand } from "../abstracts/BaseCommand.js" -import type { CommandInteraction } from "../internals/CommandInteraction.js" import type { AutocompleteInteraction } from "../internals/AutocompleteInteraction.js" +import type { CommandInteraction } from "../internals/CommandInteraction.js" export type CommandOptions = APIApplicationCommandBasicOption[] diff --git a/packages/carbon/src/internals/AutocompleteInteraction.ts b/packages/carbon/src/internals/AutocompleteInteraction.ts index 0204af3a..ea3484f1 100644 --- a/packages/carbon/src/internals/AutocompleteInteraction.ts +++ b/packages/carbon/src/internals/AutocompleteInteraction.ts @@ -1,14 +1,14 @@ -import type { BaseCommand } from "../abstracts/BaseCommand.js" -import { BaseInteraction } from "../abstracts/BaseInteraction.js" import { + type APIApplicationCommandAutocompleteInteraction, type APIApplicationCommandInteractionDataBasicOption, ApplicationCommandOptionType, ApplicationCommandType, + InteractionResponseType, InteractionType, - type APIApplicationCommandAutocompleteInteraction, - Routes, - InteractionResponseType + Routes } from "discord-api-types/v10" +import type { BaseCommand } from "../abstracts/BaseCommand.js" +import { BaseInteraction } from "../abstracts/BaseInteraction.js" import type { Client } from "../classes/Client.js" import { Command } from "../classes/Command.js" import { OptionsHandler } from "./OptionsHandler.js" diff --git a/packages/carbon/src/internals/CommandHandler.ts b/packages/carbon/src/internals/CommandHandler.ts index 10636473..7918f9b6 100644 --- a/packages/carbon/src/internals/CommandHandler.ts +++ b/packages/carbon/src/internals/CommandHandler.ts @@ -9,8 +9,8 @@ import { Base } from "../abstracts/Base.js" import { Command } from "../classes/Command.js" import { CommandWithSubcommandGroups } from "../classes/CommandWithSubcommandGroups.js" import { CommandWithSubcommands } from "../classes/CommandWithSubcommands.js" -import { CommandInteraction } from "./CommandInteraction.js" import { AutocompleteInteraction } from "./AutocompleteInteraction.js" +import { CommandInteraction } from "./CommandInteraction.js" export class CommandHandler extends Base { private getCommand( diff --git a/packages/carbon/src/structures/Guild.ts b/packages/carbon/src/structures/Guild.ts index 47430c0b..4d4b2c63 100644 --- a/packages/carbon/src/structures/Guild.ts +++ b/packages/carbon/src/structures/Guild.ts @@ -1,6 +1,7 @@ import { type APIChannel, type APIGuild, + type APIGuildMember, type APIRole, type RESTPostAPIGuildRoleJSONBody, Routes @@ -8,6 +9,7 @@ import { import { Base } from "../abstracts/Base.js" import type { Client } from "../classes/Client.js" import { channelFactory } from "../factories/channelFactory.js" +import { GuildMember } from "./GuildMember.js" import { Role } from "./Role.js" export class Guild extends Base { @@ -103,6 +105,16 @@ export class Guild extends Base { return new Role(this.client, role) } + /** + * Get a member in the guild by ID + */ + async fetchMember(memberId: string) { + const member = (await this.client.rest.get( + Routes.guildMember(this.id, memberId) + )) as APIGuildMember + return new GuildMember(this.client, member, this) + } + /** * Get the URL of the guild's icon */ diff --git a/packages/carbon/src/structures/GuildMember.ts b/packages/carbon/src/structures/GuildMember.ts new file mode 100644 index 00000000..8adec3d5 --- /dev/null +++ b/packages/carbon/src/structures/GuildMember.ts @@ -0,0 +1,231 @@ +import type { APIGuildMember, GuildMemberFlags } from "discord-api-types/v10" +import { Base } from "../abstracts/Base.js" +import type { Client } from "../classes/Client.js" +import type { Guild } from "./Guild.js" +import { Role } from "./Role.js" +import { User } from "./User.js" + +export class GuildMember extends Base { + /** + * The guild-specific nickname of the member. + */ + nickname?: string | null + /** + * The guild-specific avatar hash of the member. + * You can use {@link GuildMember.avatarUrl} to get the URL of the avatar. + */ + avatar?: string | null + /** + * Is this member muted in Voice Channels? + */ + mute?: boolean | null + /** + * Is this member deafened in Voice Channels? + */ + deaf?: boolean | null + /** + * The date since this member boosted the guild, if applicable. + */ + premiumSince?: string | null + /** + * The flags of the member. + * @see https://discord.com/developers/docs/resources/guild#guild-member-object-guild-member-flags + */ + flags?: GuildMemberFlags | null + /** + * The roles of the member + */ + roles?: Role[] | null + /** + * The joined date of the member + */ + joinedAt?: string | null + /** + * The date when the member's communication privileges (timeout) will be reinstated + */ + communicationDisabledUntil?: string | null + /** + * Is this member yet to pass the guild's Membership Screening requirements? + */ + pending?: boolean | null + /** + * The guild object of the member + */ + guild: Guild + /** + * The user object of the member + */ + user?: User | null + + private rawData: APIGuildMember | null = null + + constructor(client: Client, rawData: APIGuildMember, guild: Guild) { + super(client) + this.rawData = rawData + this.guild = guild + this.setData(rawData) + } + + private setData(data: typeof this.rawData) { + if (!data) throw new Error("Cannot set data without having data... smh") + this.rawData = data + this.nickname = data.nick + this.avatar = data.avatar + this.mute = data.mute + this.deaf = data.deaf + this.premiumSince = data.premium_since + this.flags = data.flags + this.roles = data.roles?.map((roleId) => new Role(this.client, roleId)) + this.joinedAt = data.joined_at + this.communicationDisabledUntil = data.communication_disabled_until + this.pending = data.pending + this.user = data.user ? new User(this.client, data.user) : null + } + + /** + * Set the nickname of the member + */ + async setNickname(nickname: string | null): Promise { + await this.client.rest.patch( + `/guilds/${this.guild?.id}/members/${this.user?.id}`, + { + body: { + nick: nickname + } + } + ) + this.nickname = nickname + } + + /** + * Add a role to the member + */ + async addRole(roleId: string): Promise { + await this.client.rest.put( + `/guilds/${this.guild?.id}/members/${this.user?.id}/roles/${roleId}`, + {} + ) + this.roles?.push(new Role(this.client, roleId)) + } + + /** + * Remove a role from the member + */ + async removeRole(roleId: string): Promise { + await this.client.rest.delete( + `/guilds/${this.guild?.id}/members/${this.user?.id}/roles/${roleId}` + ) + this.roles = this.roles?.filter((role) => role.id !== roleId) + } + + /** + * Kick the member from the guild + */ + async kick(): Promise { + await this.client.rest.delete( + `/guilds/${this.guild?.id}/members/${this.user?.id}` + ) + } + + /** + * Ban the member from the guild + */ + async ban( + options: { reason?: string; deleteMessageDays?: number } = {} + ): Promise { + await this.client.rest.put( + `/guilds/${this.guild?.id}/bans/${this.user?.id}`, + { + body: { + reason: options.reason, + delete_message_days: options.deleteMessageDays + } + } + ) + } + + /** + * Mute a member in voice channels + */ + async muteMember(): Promise { + await this.client.rest.patch( + `/guilds/${this.guild?.id}/members/${this.user?.id}`, + { + body: { + mute: true + } + } + ) + this.mute = true + } + + /** + * Unmute a member in voice channels + */ + async unmuteMember(): Promise { + await this.client.rest.patch( + `/guilds/${this.guild?.id}/members/${this.user?.id}`, + { + body: { + mute: false + } + } + ) + this.mute = false + } + + /** + * Deafen a member in voice channels + */ + async deafenMember(): Promise { + await this.client.rest.patch( + `/guilds/${this.guild?.id}/members/${this.user?.id}`, + { + body: { + deaf: true + } + } + ) + this.deaf = true + } + + /** + * Undeafen a member in voice channels + */ + async undeafenMember(): Promise { + await this.client.rest.patch( + `/guilds/${this.guild?.id}/members/${this.user?.id}`, + { + body: { + deaf: false + } + } + ) + this.deaf = false + } + + /** + * Set or remove a timeout for a member in the guild + */ + async timeoutMember(communicationDisabledUntil: string): Promise { + await this.client.rest.patch( + `/guilds/${this.guild?.id}/members/${this.user?.id}`, + { + body: { + communication_disabled_until: communicationDisabledUntil + } + } + ) + this.communicationDisabledUntil = communicationDisabledUntil + } + + /** + * Get the URL of the member's guild-specific avatar + */ + get avatarUrl(): string | null { + if (!this.user) return null + return this.avatar + ? `https://cdn.discordapp.com/guilds/${this.guild.id}/users/${this.user?.id}/${this.avatar}.png` + : null + } +} diff --git a/website/app/[...slug]/page.tsx b/website/app/[...slug]/page.tsx index b9196ad5..4ef6c8a5 100644 --- a/website/app/[...slug]/page.tsx +++ b/website/app/[...slug]/page.tsx @@ -30,7 +30,6 @@ export default async function Page({ return (