From 2683a24c4a75631a22334c471f2e6b86127e9078 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sat, 12 Jul 2025 23:09:07 +0200 Subject: [PATCH 1/6] feat: implement structures in mainlib --- packages/next/package.json | 1 + packages/next/src/Client.ts | 50 +++++++++++++++++ packages/next/src/managers/CachedManager.ts | 41 ++++++++++++++ packages/next/src/managers/ChannelManager.ts | 14 +++++ .../src/structures/AnnouncementChannel.ts | 51 ++++++++++++++++++ .../structures/AnnouncementThreadChannel.ts | 54 +++++++++++++++++++ .../next/src/structures/CategoryChannel.ts | 35 ++++++++++++ packages/next/src/structures/Channel.ts | 19 +++++++ packages/next/src/structures/DMChannel.ts | 34 ++++++++++++ packages/next/src/structures/ForumChannel.ts | 54 +++++++++++++++++++ .../next/src/structures/GroupDMChannel.ts | 37 +++++++++++++ packages/next/src/structures/MediaChannel.ts | 45 ++++++++++++++++ .../src/structures/PrivateThreadChannel.ts | 51 ++++++++++++++++++ .../src/structures/PublicThreadChannel.ts | 54 +++++++++++++++++++ packages/next/src/structures/StageChannel.ts | 45 ++++++++++++++++ packages/next/src/structures/TextChannel.ts | 48 +++++++++++++++++ packages/next/src/structures/VoiceChannel.ts | 45 ++++++++++++++++ .../src/structures/mixins/BaseChannelMixin.ts | 23 ++++++++ packages/next/src/util/types.ts | 9 ++++ packages/structures/src/index.ts | 1 + pnpm-lock.yaml | 3 ++ 21 files changed, 714 insertions(+) create mode 100644 packages/next/src/Client.ts create mode 100644 packages/next/src/managers/CachedManager.ts create mode 100644 packages/next/src/managers/ChannelManager.ts create mode 100644 packages/next/src/structures/AnnouncementChannel.ts create mode 100644 packages/next/src/structures/AnnouncementThreadChannel.ts create mode 100644 packages/next/src/structures/CategoryChannel.ts create mode 100644 packages/next/src/structures/Channel.ts create mode 100644 packages/next/src/structures/DMChannel.ts create mode 100644 packages/next/src/structures/ForumChannel.ts create mode 100644 packages/next/src/structures/GroupDMChannel.ts create mode 100644 packages/next/src/structures/MediaChannel.ts create mode 100644 packages/next/src/structures/PrivateThreadChannel.ts create mode 100644 packages/next/src/structures/PublicThreadChannel.ts create mode 100644 packages/next/src/structures/StageChannel.ts create mode 100644 packages/next/src/structures/TextChannel.ts create mode 100644 packages/next/src/structures/VoiceChannel.ts create mode 100644 packages/next/src/structures/mixins/BaseChannelMixin.ts create mode 100644 packages/next/src/util/types.ts diff --git a/packages/next/package.json b/packages/next/package.json index ae440a409269..25bf6c0f5e73 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -70,6 +70,7 @@ "@discordjs/core": "workspace:^", "@discordjs/formatters": "workspace:^", "@discordjs/rest": "workspace:^", + "@discordjs/structures": "workspace:^", "@discordjs/util": "workspace:^", "@discordjs/ws": "workspace:^", "discord-api-types": "^0.38.15" diff --git a/packages/next/src/Client.ts b/packages/next/src/Client.ts new file mode 100644 index 000000000000..1a1a95c2a298 --- /dev/null +++ b/packages/next/src/Client.ts @@ -0,0 +1,50 @@ +import process from 'node:process'; +import { Collection } from '@discordjs/collection'; +import type { RESTGetAPIGatewayBotResult, Snowflake } from '@discordjs/core'; +import { Client as CoreClient, Routes, GatewayDispatchEvents } from '@discordjs/core'; +import { REST } from '@discordjs/rest'; +import type { Structure } from '@discordjs/structures'; +import { WebSocketManager } from '@discordjs/ws'; +import type { CacheConstructor, Cache } from './util/types'; + +export class Client { + private core!: CoreClient; + + public options: { token: string | undefined }; + + #token: string; + + public CacheConstructor>(_value: new (...args: any[]) => Value) { + return new this.cacheConstructor() as Cache; + } + + protected cacheConstructor: CacheConstructor; + + public constructor({ token = process.env.DISCORD_TOKEN }: { token?: string }, cache: CacheConstructor = Collection) { + this.options = { token }; + this.#token = token!; + this.cacheConstructor = cache; + } + + public async login(token?: string) { + if (token) { + this.#token = token; + } + + const rest = new REST().setToken(this.#token); + const gateway = new WebSocketManager({ + fetchGatewayInformation: async () => rest.get(Routes.gatewayBot()) as Promise, + intents: 0, + token: this.#token, + }); + this.core = new CoreClient({ + gateway, + rest, + }); + + // TODO: remove once we actually implement anything using this.core + this.core.on(GatewayDispatchEvents.Ready, console.log); + + await gateway.connect(); + } +} diff --git a/packages/next/src/managers/CachedManager.ts b/packages/next/src/managers/CachedManager.ts new file mode 100644 index 000000000000..f41f2d46eea2 --- /dev/null +++ b/packages/next/src/managers/CachedManager.ts @@ -0,0 +1,41 @@ +import type { Structure } from '@discordjs/structures'; +import { kClone, kPatch } from '@discordjs/structures'; +import type { Snowflake } from 'discord-api-types/globals'; +import type { Client } from '../Client.js'; +import type { Cache } from '../util/types.js'; + +export class CachedlManager & { get id(): Snowflake }> { + public client: Client; + + public cache: Cache; + + private readonly holds: new (...args: any[]) => Value; + + public constructor(client: Client, value: new (...args: any[]) => Value) { + this.client = client; + this.cache = client.CacheConstructor(value); + this.holds = value; + } + + protected async _add( + data: Value extends Structure ? (Type extends { id: Snowflake } ? Type : never) : never, + cache = true, + { id, extras = [] }: { extras?: unknown[]; id?: Snowflake } = {}, + ) { + const existing = await this.cache.get(id ?? data.id); + if (existing) { + if (cache) { + existing[kPatch](data); + return existing; + } + + const clone = existing[kClone](); + clone[kPatch](data); + return clone; + } + + const entry = this.holds ? new this.holds(data, this.client, ...extras) : data; + if (cache) await this.cache.set(id ?? entry.id, entry); + return entry; + } +} diff --git a/packages/next/src/managers/ChannelManager.ts b/packages/next/src/managers/ChannelManager.ts new file mode 100644 index 000000000000..7c60593ebfd3 --- /dev/null +++ b/packages/next/src/managers/ChannelManager.ts @@ -0,0 +1,14 @@ +import type { APIChannel } from 'discord-api-types/v10'; +import type { Client } from '../Client.js'; +import { Channel } from '../structures/Channel.js'; +import { CachedlManager } from './CachedManager.js'; + +export class ChannelManager extends CachedlManager { + public constructor(data: APIChannel[], client: Client) { + super(client, Channel); + + for (const channel of data) { + this.cache.set(channel.id, new Channel(channel, client)); + } + } +} diff --git a/packages/next/src/structures/AnnouncementChannel.ts b/packages/next/src/structures/AnnouncementChannel.ts new file mode 100644 index 000000000000..07ed98d0a1dd --- /dev/null +++ b/packages/next/src/structures/AnnouncementChannel.ts @@ -0,0 +1,51 @@ +import { + Channel, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + ChannelTopicMixin, + Mixin, + type MixinTypes, + type Partialize, + TextChannelMixin, +} from '@discordjs/structures'; +import type { APINewsChannel, ChannelType } from 'discord-api-types/v10'; +import { BaseChannelMixin } from './mixins/BaseChannelMixin.js'; + +export interface AnnouncementChannel + extends MixinTypes< + Channel, + [ + BaseChannelMixin, + TextChannelMixin, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + ChannelTopicMixin, + ] + > {} + +/** + * Sample Implementation of a structure for announcement channels, usable by direct end consumers. + */ +export class AnnouncementChannel extends Channel< + ChannelType.GuildAnnouncement, + Omitted +> { + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } +} + +Mixin(AnnouncementChannel, [ + BaseChannelMixin, + TextChannelMixin, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + ChannelTopicMixin, +]); diff --git a/packages/next/src/structures/AnnouncementThreadChannel.ts b/packages/next/src/structures/AnnouncementThreadChannel.ts new file mode 100644 index 000000000000..6e0bcdeaba24 --- /dev/null +++ b/packages/next/src/structures/AnnouncementThreadChannel.ts @@ -0,0 +1,54 @@ +import { + Mixin, + type MixinTypes, + type Partialize, + Channel, + ChannelOwnerMixin, + ChannelParentMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + GuildChannelMixin, + TextChannelMixin, + ThreadChannelMixin, +} from '@discordjs/structures'; +import type { APIAnnouncementThreadChannel, ChannelType } from 'discord-api-types/v10'; +import { BaseChannelMixin } from './mixins/BaseChannelMixin'; + +export interface AnnouncementThreadChannel + extends MixinTypes< + Channel, + [ + BaseChannelMixin, + TextChannelMixin, + ChannelOwnerMixin, + ChannelParentMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + GuildChannelMixin, + ThreadChannelMixin, + ] + > {} + +/** + * Sample Implementation of a structure for announcement threads, usable by direct end consumers. + */ +export class AnnouncementThreadChannel extends Channel< + ChannelType.AnnouncementThread, + Omitted +> { + public constructor(data: Partialize) { + super(data); + this.optimizeData?.(data); + } +} + +Mixin(AnnouncementThreadChannel, [ + BaseChannelMixin, + TextChannelMixin, + ChannelOwnerMixin, + ChannelParentMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + GuildChannelMixin, + ThreadChannelMixin, +]); diff --git a/packages/next/src/structures/CategoryChannel.ts b/packages/next/src/structures/CategoryChannel.ts new file mode 100644 index 000000000000..72b4e9c522d0 --- /dev/null +++ b/packages/next/src/structures/CategoryChannel.ts @@ -0,0 +1,35 @@ +import { + Mixin, + type MixinTypes, + type Partialize, + Channel, + ChannelPermissionMixin, + GuildChannelMixin, +} from '@discordjs/structures'; +import type { APIGuildCategoryChannel, ChannelType } from 'discord-api-types/v10'; +import { BaseChannelMixin } from './mixins/BaseChannelMixin'; + +export interface CategoryChannel + extends MixinTypes< + Channel, + [ + BaseChannelMixin, + ChannelPermissionMixin, + GuildChannelMixin, + ] + > {} + +/** + * Sample Implementation of a structure for category channels, usable by direct end consumers. + */ +export class CategoryChannel extends Channel< + ChannelType.GuildCategory, + Omitted +> { + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } +} + +Mixin(CategoryChannel, [BaseChannelMixin, ChannelPermissionMixin, GuildChannelMixin]); diff --git a/packages/next/src/structures/Channel.ts b/packages/next/src/structures/Channel.ts new file mode 100644 index 000000000000..5fa34473f893 --- /dev/null +++ b/packages/next/src/structures/Channel.ts @@ -0,0 +1,19 @@ +import type { MixinTypes } from '@discordjs/structures'; +import { Mixin, Channel as StructureChannel } from '@discordjs/structures'; +import type { APIChannel } from 'discord-api-types/v10'; +import type { Client } from '../Client.js'; +import { BaseChannelMixin } from './mixins/BaseChannelMixin'; + +export interface Channel extends MixinTypes {} + +export class Channel extends StructureChannel { + public readonly client: Client; + + public constructor(data: APIChannel, client: Client) { + super(data); + + this.client = client; + } +} + +Mixin(Channel, [BaseChannelMixin]); diff --git a/packages/next/src/structures/DMChannel.ts b/packages/next/src/structures/DMChannel.ts new file mode 100644 index 000000000000..5578b8dfca9c --- /dev/null +++ b/packages/next/src/structures/DMChannel.ts @@ -0,0 +1,34 @@ +import { + Mixin, + type MixinTypes, + type Partialize, + Channel, + ChannelPinMixin, + DMChannelMixin, + TextChannelMixin, +} from '@discordjs/structures'; +import type { APIDMChannel, ChannelType } from 'discord-api-types/v10'; +import { BaseChannelMixin } from './mixins/BaseChannelMixin'; + +export interface DMChannel + extends MixinTypes< + Channel, + [ + BaseChannelMixin, + DMChannelMixin, + TextChannelMixin, + ChannelPinMixin, + ] + > {} + +/** + * Sample Implementation of a structure for dm channels, usable by direct end consumers. + */ +export class DMChannel extends Channel { + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } +} + +Mixin(DMChannel, [BaseChannelMixin, DMChannelMixin, TextChannelMixin, ChannelPinMixin]); diff --git a/packages/next/src/structures/ForumChannel.ts b/packages/next/src/structures/ForumChannel.ts new file mode 100644 index 000000000000..4ac03391d381 --- /dev/null +++ b/packages/next/src/structures/ForumChannel.ts @@ -0,0 +1,54 @@ +import { + Mixin, + type MixinTypes, + kData, + type Partialize, + Channel, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelTopicMixin, + ThreadOnlyChannelMixin, +} from '@discordjs/structures'; +import type { APIGuildForumChannel, ChannelType } from 'discord-api-types/v10'; +import { BaseChannelMixin } from './mixins/BaseChannelMixin'; + +export interface ForumChannel + extends MixinTypes< + Channel, + [ + BaseChannelMixin, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelTopicMixin, + ThreadOnlyChannelMixin, + ] + > {} + +/** + * Sample Implementation of a structure for forum channels, usable by direct end consumers. + */ +export class ForumChannel extends Channel< + ChannelType.GuildForum, + Omitted +> { + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } + + /** + * The default forum layout view used to display posts in this channel. + * Defaults to 0, which indicates a layout view has not been set by a channel admin. + */ + public get defaultForumLayout() { + return this[kData].default_forum_layout; + } +} + +Mixin(ForumChannel, [ + BaseChannelMixin, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelTopicMixin, + ThreadOnlyChannelMixin, +]); diff --git a/packages/next/src/structures/GroupDMChannel.ts b/packages/next/src/structures/GroupDMChannel.ts new file mode 100644 index 000000000000..c3dbf0201d1b --- /dev/null +++ b/packages/next/src/structures/GroupDMChannel.ts @@ -0,0 +1,37 @@ +import { + Mixin, + type MixinTypes, + type Partialize, + Channel, + ChannelOwnerMixin, + DMChannelMixin, + GroupDMMixin, + TextChannelMixin, +} from '@discordjs/structures'; +import type { APIGroupDMChannel, ChannelType } from 'discord-api-types/v10'; + +export interface GroupDMChannel + extends MixinTypes< + Channel, + [ + DMChannelMixin, + TextChannelMixin, + ChannelOwnerMixin, + GroupDMMixin, + ] + > {} + +/** + * Sample Implementation of a structure for group dm channels, usable by direct end consumers. + */ +export class GroupDMChannel extends Channel< + ChannelType.GroupDM, + Omitted +> { + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } +} + +Mixin(GroupDMChannel, [DMChannelMixin, TextChannelMixin, ChannelOwnerMixin, GroupDMMixin]); diff --git a/packages/next/src/structures/MediaChannel.ts b/packages/next/src/structures/MediaChannel.ts new file mode 100644 index 000000000000..9f315ba3bb06 --- /dev/null +++ b/packages/next/src/structures/MediaChannel.ts @@ -0,0 +1,45 @@ +import { + Mixin, + type MixinTypes, + type Partialize, + Channel, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelTopicMixin, + ThreadOnlyChannelMixin, +} from '@discordjs/structures'; +import type { APIGuildMediaChannel, ChannelType } from 'discord-api-types/v10'; +import { BaseChannelMixin } from './mixins/BaseChannelMixin'; + +export interface MediaChannel + extends MixinTypes< + Channel, + [ + BaseChannelMixin, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelTopicMixin, + ThreadOnlyChannelMixin, + ] + > {} + +/** + * Sample Implementation of a structure for media channels, usable by direct end consumers. + */ +export class MediaChannel extends Channel< + ChannelType.GuildMedia, + Omitted +> { + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } +} + +Mixin(MediaChannel, [ + BaseChannelMixin, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelTopicMixin, + ThreadOnlyChannelMixin, +]); diff --git a/packages/next/src/structures/PrivateThreadChannel.ts b/packages/next/src/structures/PrivateThreadChannel.ts new file mode 100644 index 000000000000..3c1b0fed7abf --- /dev/null +++ b/packages/next/src/structures/PrivateThreadChannel.ts @@ -0,0 +1,51 @@ +import { + Mixin, + type MixinTypes, + type Partialize, + Channel, + ChannelOwnerMixin, + ChannelParentMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + TextChannelMixin, + ThreadChannelMixin, +} from '@discordjs/structures'; +import type { APIPrivateThreadChannel, ChannelType } from 'discord-api-types/v10'; +import { BaseChannelMixin } from './mixins/BaseChannelMixin'; + +export interface PrivateThreadChannel + extends MixinTypes< + Channel, + [ + BaseChannelMixin, + TextChannelMixin, + ChannelOwnerMixin, + ChannelParentMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + ThreadChannelMixin, + ] + > {} + +/** + * Sample Implementation of a structure for private thread channels, usable by direct end consumers. + */ +export class PrivateThreadChannel extends Channel< + ChannelType.PrivateThread, + Omitted +> { + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } +} + +Mixin(PrivateThreadChannel, [ + BaseChannelMixin, + TextChannelMixin, + ChannelOwnerMixin, + ChannelParentMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + ThreadChannelMixin, +]); diff --git a/packages/next/src/structures/PublicThreadChannel.ts b/packages/next/src/structures/PublicThreadChannel.ts new file mode 100644 index 000000000000..d175504653e1 --- /dev/null +++ b/packages/next/src/structures/PublicThreadChannel.ts @@ -0,0 +1,54 @@ +import { + Mixin, + type MixinTypes, + type Partialize, + Channel, + AppliedTagsMixin, + ChannelOwnerMixin, + ChannelParentMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + TextChannelMixin, + ThreadChannelMixin, +} from '@discordjs/structures'; +import type { APIPublicThreadChannel, ChannelType } from 'discord-api-types/v10'; +import { BaseChannelMixin } from './mixins/BaseChannelMixin'; + +export interface PublicThreadChannel + extends MixinTypes< + Channel, + [ + BaseChannelMixin, + TextChannelMixin, + ChannelOwnerMixin, + ChannelParentMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + ThreadChannelMixin, + AppliedTagsMixin, + ] + > {} + +/** + * Sample Implementation of a structure for public thread channels, usable by direct end consumers. + */ +export class PublicThreadChannel extends Channel< + ChannelType.PublicThread, + Omitted +> { + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } +} + +Mixin(PublicThreadChannel, [ + BaseChannelMixin, + TextChannelMixin, + ChannelOwnerMixin, + ChannelParentMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + ThreadChannelMixin, + AppliedTagsMixin, +]); diff --git a/packages/next/src/structures/StageChannel.ts b/packages/next/src/structures/StageChannel.ts new file mode 100644 index 000000000000..f0b711e572c6 --- /dev/null +++ b/packages/next/src/structures/StageChannel.ts @@ -0,0 +1,45 @@ +import { + Mixin, + type MixinTypes, + type Partialize, + Channel, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelSlowmodeMixin, + ChannelWebhookMixin, + VoiceChannelMixin, +} from '@discordjs/structures'; +import type { APIGuildStageVoiceChannel, ChannelType } from 'discord-api-types/v10'; +import { BaseChannelMixin } from './mixins/BaseChannelMixin'; + +export interface StageChannel + extends MixinTypes< + Channel, + [ + BaseChannelMixin, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelSlowmodeMixin, + ChannelWebhookMixin, + VoiceChannelMixin, + ] + > {} + +export class StageChannel extends Channel< + ChannelType.GuildStageVoice, + Omitted +> { + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } +} + +Mixin(StageChannel, [ + BaseChannelMixin, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelSlowmodeMixin, + ChannelWebhookMixin, + VoiceChannelMixin, +]); diff --git a/packages/next/src/structures/TextChannel.ts b/packages/next/src/structures/TextChannel.ts new file mode 100644 index 000000000000..3356279f21f7 --- /dev/null +++ b/packages/next/src/structures/TextChannel.ts @@ -0,0 +1,48 @@ +import { + Mixin, + type MixinTypes, + type Partialize, + Channel, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + ChannelTopicMixin, + TextChannelMixin, +} from '@discordjs/structures'; +import type { APITextChannel, ChannelType } from 'discord-api-types/v10'; +import { BaseChannelMixin } from './mixins/BaseChannelMixin'; + +export interface TextChannel + extends MixinTypes< + Channel, + [ + BaseChannelMixin, + TextChannelMixin, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + ChannelTopicMixin, + ] + > {} + +export class TextChannel extends Channel< + ChannelType.GuildText, + Omitted +> { + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } +} + +Mixin(TextChannel, [ + BaseChannelMixin, + TextChannelMixin, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelPinMixin, + ChannelSlowmodeMixin, + ChannelTopicMixin, +]); diff --git a/packages/next/src/structures/VoiceChannel.ts b/packages/next/src/structures/VoiceChannel.ts new file mode 100644 index 000000000000..97e095fc8d8f --- /dev/null +++ b/packages/next/src/structures/VoiceChannel.ts @@ -0,0 +1,45 @@ +import { + Mixin, + type MixinTypes, + type Partialize, + Channel, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelSlowmodeMixin, + ChannelWebhookMixin, + VoiceChannelMixin, +} from '@discordjs/structures'; +import type { APIGuildVoiceChannel, ChannelType } from 'discord-api-types/v10'; +import { BaseChannelMixin } from './mixins/BaseChannelMixin'; + +export interface VoiceChannel + extends MixinTypes< + Channel, + [ + BaseChannelMixin, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelSlowmodeMixin, + ChannelWebhookMixin, + VoiceChannelMixin, + ] + > {} + +export class VoiceChannel extends Channel< + ChannelType.GuildVoice, + Omitted +> { + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } +} + +Mixin(VoiceChannel, [ + BaseChannelMixin, + ChannelParentMixin, + ChannelPermissionMixin, + ChannelSlowmodeMixin, + ChannelWebhookMixin, + VoiceChannelMixin, +]); diff --git a/packages/next/src/structures/mixins/BaseChannelMixin.ts b/packages/next/src/structures/mixins/BaseChannelMixin.ts new file mode 100644 index 000000000000..16bca18ec757 --- /dev/null +++ b/packages/next/src/structures/mixins/BaseChannelMixin.ts @@ -0,0 +1,23 @@ +/* eslint-disable @typescript-eslint/dot-notation */ +import { equal } from 'node:assert'; +import type { ChannelType } from '@discordjs/core'; +import { kPatch, type Channel, type ChannelDataType } from '@discordjs/structures'; +import type { Client } from '../../Client.js'; + +export interface BaseChannelMixin extends Channel {} + +export class BaseChannelMixin { + declare public readonly client: Client; + + public async delete() { + await this.client['core'].api.channels.delete(this.id); + return this; + } + + public async fetch() { + const data = await this.client['core'].api.channels.get(this.id); + + equal(data.type, this.type); + return this[kPatch](data as ChannelDataType); + } +} diff --git a/packages/next/src/util/types.ts b/packages/next/src/util/types.ts new file mode 100644 index 000000000000..e07c3d3d1f03 --- /dev/null +++ b/packages/next/src/util/types.ts @@ -0,0 +1,9 @@ +import type { Structure } from '@discordjs/structures'; +import type { Awaitable } from '@discordjs/util'; + +export interface Cache> { + get(key: Key): Awaitable; + set(key: Key, value: Type): Awaitable; +} + +export type CacheConstructor = new (...args: any[]) => Cache>; diff --git a/packages/structures/src/index.ts b/packages/structures/src/index.ts index 5a2e6b407d7f..14cda8551a17 100644 --- a/packages/structures/src/index.ts +++ b/packages/structures/src/index.ts @@ -5,5 +5,6 @@ export * from './users/index.js'; export * from './Structure.js'; export * from './Mixin.js'; export * from './utils/optimization.js'; +export * from './utils/symbols.js'; export type * from './utils/types.js'; export type * from './MixinTypes.d.ts'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9eea1ab3590..434765707d3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1211,6 +1211,9 @@ importers: '@discordjs/rest': specifier: workspace:^ version: link:../rest + '@discordjs/structures': + specifier: workspace:^ + version: link:../structures '@discordjs/util': specifier: workspace:^ version: link:../util From 5721556660c8137765452f4a271ee23bdb30483f Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sat, 12 Jul 2025 23:23:27 +0200 Subject: [PATCH 2/6] fix: eslint --- eslint.config.js | 9 +++++---- packages/discord.js/typings/index.d.ts | 7 ++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 461563d6d26b..01b4ff7c00f7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -165,10 +165,10 @@ export default tseslint.config( 'jsdoc/no-undefined-types': 0, 'jsdoc/no-defaults': 0, 'no-eq-null': 0, - strict: ['error', 'global'], + strict: [2, 'global'], 'no-restricted-syntax': [ - 'error', + 2, { selector: "AssignmentExpression[left.object.name='module'][left.property.name='exports']", message: 'Use named exports instead of module.exports', @@ -185,7 +185,7 @@ export default tseslint.config( files: [`packages/discord.js/src/client/websocket/handlers/*.js`], rules: { 'no-restricted-syntax': [ - 'error', + 2, { selector: "VariableDeclarator[init.callee.name='require'][init.arguments.0.value=/^\\./]:not([id.type='ObjectPattern'])", @@ -199,6 +199,7 @@ export default tseslint.config( rules: { '@typescript-eslint/no-unsafe-declaration-merging': 0, '@typescript-eslint/no-empty-object-type': 0, + '@typescript-eslint/no-unused-vars': 2, '@typescript-eslint/no-use-before-define': 0, '@typescript-eslint/consistent-type-imports': 0, '@stylistic/ts/lines-between-class-members': 0, @@ -245,7 +246,7 @@ export default tseslint.config( }, }, { - files: [`packages/structures/**/*${commonFiles}`], + files: [`packages/{next,structures}/**/*${commonFiles}`], rules: { '@typescript-eslint/no-empty-interface': 0, '@typescript-eslint/no-empty-object-type': 0, diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 3a786889f74f..b2cd3024ad6a 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -31,10 +31,7 @@ import { APIComponentInModalActionRow, APIContainerComponent, APIEmbed, - APIEmbedAuthor, APIEmbedField, - APIEmbedFooter, - APIEmbedImage, APIEmbedProvider, APIEmoji, APIEntitlement, @@ -3392,7 +3389,7 @@ export interface PrivateThreadChannel extends ThreadChannel { type: ChannelType.PrivateThread; } -export interface ThreadChannel +export interface ThreadChannel extends TextBasedChannelFields, PinnableChannelFields, BulkDeleteMethod, @@ -3672,7 +3669,7 @@ export class VoiceState extends Base { public fetch(force?: boolean): Promise; } -export interface Webhook extends WebhookFields {} +export interface Webhook extends WebhookFields {} export class Webhook { private constructor(client: Client, data?: unknown); public avatar: string | null; From 5d9c9d357bcaeb786754421c513c93f28ccf41d3 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sat, 12 Jul 2025 23:34:20 +0200 Subject: [PATCH 3/6] fix: typo --- packages/next/src/managers/CachedManager.ts | 2 +- packages/next/src/managers/ChannelManager.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/src/managers/CachedManager.ts b/packages/next/src/managers/CachedManager.ts index f41f2d46eea2..4950f6489d29 100644 --- a/packages/next/src/managers/CachedManager.ts +++ b/packages/next/src/managers/CachedManager.ts @@ -4,7 +4,7 @@ import type { Snowflake } from 'discord-api-types/globals'; import type { Client } from '../Client.js'; import type { Cache } from '../util/types.js'; -export class CachedlManager & { get id(): Snowflake }> { +export class CachedManager & { get id(): Snowflake }> { public client: Client; public cache: Cache; diff --git a/packages/next/src/managers/ChannelManager.ts b/packages/next/src/managers/ChannelManager.ts index 7c60593ebfd3..457a815fc018 100644 --- a/packages/next/src/managers/ChannelManager.ts +++ b/packages/next/src/managers/ChannelManager.ts @@ -1,9 +1,9 @@ import type { APIChannel } from 'discord-api-types/v10'; import type { Client } from '../Client.js'; import { Channel } from '../structures/Channel.js'; -import { CachedlManager } from './CachedManager.js'; +import { CachedManager } from './CachedManager.js'; -export class ChannelManager extends CachedlManager { +export class ChannelManager extends CachedManager { public constructor(data: APIChannel[], client: Client) { super(client, Channel); From 082445b7b6654ff3e9dba94337e31ca30e8fcda9 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sun, 13 Jul 2025 00:01:27 +0200 Subject: [PATCH 4/6] fix: clone & patch at once --- packages/next/src/managers/CachedManager.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/next/src/managers/CachedManager.ts b/packages/next/src/managers/CachedManager.ts index 4950f6489d29..f0a84b0b0a84 100644 --- a/packages/next/src/managers/CachedManager.ts +++ b/packages/next/src/managers/CachedManager.ts @@ -29,9 +29,7 @@ export class CachedManager & { get id(): Snowfla return existing; } - const clone = existing[kClone](); - clone[kPatch](data); - return clone; + return existing[kClone](data); } const entry = this.holds ? new this.holds(data, this.client, ...extras) : data; From e90deb7e9390cf977b43deccd520401205221995 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sun, 20 Jul 2025 13:17:37 +0200 Subject: [PATCH 5/6] refactor: createStructure in managers --- packages/next/src/Client.ts | 17 +++-- packages/next/src/managers/CachedManager.ts | 24 ++++--- packages/next/src/managers/ChannelManager.ts | 66 +++++++++++++++++-- packages/next/src/structures/BaseChannel.ts | 14 ++++ packages/next/src/structures/Channel.ts | 19 ------ .../src/structures/mixins/BaseChannelMixin.ts | 5 +- .../mixins/ChannelPermissionsMixin.ts | 13 ++++ packages/next/src/util/cache.ts | 25 +++++++ packages/next/src/util/container.ts | 30 +++++++++ packages/next/src/util/types.ts | 9 --- 10 files changed, 170 insertions(+), 52 deletions(-) create mode 100644 packages/next/src/structures/BaseChannel.ts delete mode 100644 packages/next/src/structures/Channel.ts create mode 100644 packages/next/src/structures/mixins/ChannelPermissionsMixin.ts create mode 100644 packages/next/src/util/cache.ts create mode 100644 packages/next/src/util/container.ts delete mode 100644 packages/next/src/util/types.ts diff --git a/packages/next/src/Client.ts b/packages/next/src/Client.ts index 1a1a95c2a298..9dee8ce27ddf 100644 --- a/packages/next/src/Client.ts +++ b/packages/next/src/Client.ts @@ -1,11 +1,11 @@ import process from 'node:process'; -import { Collection } from '@discordjs/collection'; import type { RESTGetAPIGatewayBotResult, Snowflake } from '@discordjs/core'; import { Client as CoreClient, Routes, GatewayDispatchEvents } from '@discordjs/core'; import { REST } from '@discordjs/rest'; import type { Structure } from '@discordjs/structures'; import { WebSocketManager } from '@discordjs/ws'; -import type { CacheConstructor, Cache } from './util/types'; +import { CollectionCache, type CacheConstructor, type StructureCache } from './util/cache.js'; +import { container } from './util/container.js'; export class Client { private core!: CoreClient; @@ -14,16 +14,23 @@ export class Client { #token: string; - public CacheConstructor>(_value: new (...args: any[]) => Value) { - return new this.cacheConstructor() as Cache; + public CacheConstructor< + Value extends Structure<{ id: Snowflake }>, + Raw extends { id: Snowflake } = Value extends Structure ? Type : never, + >() { + return new this.cacheConstructor() as StructureCache; } protected cacheConstructor: CacheConstructor; - public constructor({ token = process.env.DISCORD_TOKEN }: { token?: string }, cache: CacheConstructor = Collection) { + public constructor( + { token = process.env.DISCORD_TOKEN }: { token?: string }, + cache: CacheConstructor = CollectionCache, + ) { this.options = { token }; this.#token = token!; this.cacheConstructor = cache; + container.set('client', this); } public async login(token?: string) { diff --git a/packages/next/src/managers/CachedManager.ts b/packages/next/src/managers/CachedManager.ts index f0a84b0b0a84..1e99926236cc 100644 --- a/packages/next/src/managers/CachedManager.ts +++ b/packages/next/src/managers/CachedManager.ts @@ -2,26 +2,24 @@ import type { Structure } from '@discordjs/structures'; import { kClone, kPatch } from '@discordjs/structures'; import type { Snowflake } from 'discord-api-types/globals'; import type { Client } from '../Client.js'; -import type { Cache } from '../util/types.js'; +import type { StructureCache } from '../util/cache.js'; -export class CachedManager & { get id(): Snowflake }> { +export abstract class CachedManager< + Value extends Structure<{ id: Snowflake }> & { get id(): Snowflake }, + Raw extends { id: Snowflake } = Value extends Structure ? Type : never, +> { public client: Client; - public cache: Cache; + public cache: StructureCache; - private readonly holds: new (...args: any[]) => Value; + protected abstract createStructure(data: Raw, ...extras: unknown[]): Value; - public constructor(client: Client, value: new (...args: any[]) => Value) { + public constructor(client: Client) { this.client = client; - this.cache = client.CacheConstructor(value); - this.holds = value; + this.cache = client.CacheConstructor(); } - protected async _add( - data: Value extends Structure ? (Type extends { id: Snowflake } ? Type : never) : never, - cache = true, - { id, extras = [] }: { extras?: unknown[]; id?: Snowflake } = {}, - ) { + protected async _add(data: Raw, cache = true, { id, extras = [] }: { extras?: unknown[]; id?: Snowflake } = {}) { const existing = await this.cache.get(id ?? data.id); if (existing) { if (cache) { @@ -32,7 +30,7 @@ export class CachedManager & { get id(): Snowfla return existing[kClone](data); } - const entry = this.holds ? new this.holds(data, this.client, ...extras) : data; + const entry = this.createStructure(data, ...extras); if (cache) await this.cache.set(id ?? entry.id, entry); return entry; } diff --git a/packages/next/src/managers/ChannelManager.ts b/packages/next/src/managers/ChannelManager.ts index 457a815fc018..f85f8e6891dc 100644 --- a/packages/next/src/managers/ChannelManager.ts +++ b/packages/next/src/managers/ChannelManager.ts @@ -1,14 +1,72 @@ -import type { APIChannel } from 'discord-api-types/v10'; +import { ChannelType, type APIChannel } from 'discord-api-types/v10'; import type { Client } from '../Client.js'; -import { Channel } from '../structures/Channel.js'; +import { AnnouncementChannel } from '../structures/AnnouncementChannel.js'; +import { AnnouncementThreadChannel } from '../structures/AnnouncementThreadChannel.js'; +import { BaseChannel } from '../structures/BaseChannel.js'; +import { CategoryChannel } from '../structures/CategoryChannel.js'; +import { DMChannel } from '../structures/DMChannel.js'; +import { ForumChannel } from '../structures/ForumChannel.js'; +import { GroupDMChannel } from '../structures/GroupDMChannel.js'; +import { MediaChannel } from '../structures/MediaChannel.js'; +import { PrivateThreadChannel } from '../structures/PrivateThreadChannel.js'; +import { PublicThreadChannel } from '../structures/PublicThreadChannel.js'; +import { StageChannel } from '../structures/StageChannel.js'; +import { TextChannel } from '../structures/TextChannel.js'; +import { VoiceChannel } from '../structures/VoiceChannel.js'; import { CachedManager } from './CachedManager.js'; +export type Channel = + | AnnouncementChannel + | AnnouncementThreadChannel + | BaseChannel + | CategoryChannel + | DMChannel + | ForumChannel + | GroupDMChannel + | MediaChannel + | PrivateThreadChannel + | PublicThreadChannel + | StageChannel + | TextChannel + | VoiceChannel; + export class ChannelManager extends CachedManager { public constructor(data: APIChannel[], client: Client) { - super(client, Channel); + super(client); for (const channel of data) { - this.cache.set(channel.id, new Channel(channel, client)); + this.cache.set(channel.id, this.createStructure(channel)); + } + } + + protected override createStructure(data: APIChannel): Channel { + switch (data.type) { + case ChannelType.AnnouncementThread: + return new AnnouncementThreadChannel(data); + case ChannelType.DM: + return new DMChannel(data); + case ChannelType.GroupDM: + return new GroupDMChannel(data); + case ChannelType.GuildAnnouncement: + return new AnnouncementChannel(data); + case ChannelType.GuildCategory: + return new CategoryChannel(data); + case ChannelType.GuildForum: + return new ForumChannel(data); + case ChannelType.GuildMedia: + return new MediaChannel(data); + case ChannelType.GuildStageVoice: + return new StageChannel(data); + case ChannelType.GuildText: + return new TextChannel(data); + case ChannelType.GuildVoice: + return new VoiceChannel(data); + case ChannelType.PrivateThread: + return new PrivateThreadChannel(data); + case ChannelType.PublicThread: + return new PublicThreadChannel(data); + default: + return new BaseChannel(data); } } } diff --git a/packages/next/src/structures/BaseChannel.ts b/packages/next/src/structures/BaseChannel.ts new file mode 100644 index 000000000000..92652a225b3d --- /dev/null +++ b/packages/next/src/structures/BaseChannel.ts @@ -0,0 +1,14 @@ +import type { MixinTypes } from '@discordjs/structures'; +import { Mixin, Channel as StructureChannel } from '@discordjs/structures'; +import type { APIChannel } from 'discord-api-types/v10'; +import { BaseChannelMixin } from './mixins/BaseChannelMixin'; + +export interface BaseChannel extends MixinTypes {} + +export class BaseChannel extends StructureChannel { + public constructor(data: APIChannel) { + super(data); + } +} + +Mixin(BaseChannel, [BaseChannelMixin]); diff --git a/packages/next/src/structures/Channel.ts b/packages/next/src/structures/Channel.ts deleted file mode 100644 index 5fa34473f893..000000000000 --- a/packages/next/src/structures/Channel.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { MixinTypes } from '@discordjs/structures'; -import { Mixin, Channel as StructureChannel } from '@discordjs/structures'; -import type { APIChannel } from 'discord-api-types/v10'; -import type { Client } from '../Client.js'; -import { BaseChannelMixin } from './mixins/BaseChannelMixin'; - -export interface Channel extends MixinTypes {} - -export class Channel extends StructureChannel { - public readonly client: Client; - - public constructor(data: APIChannel, client: Client) { - super(data); - - this.client = client; - } -} - -Mixin(Channel, [BaseChannelMixin]); diff --git a/packages/next/src/structures/mixins/BaseChannelMixin.ts b/packages/next/src/structures/mixins/BaseChannelMixin.ts index 16bca18ec757..aa412f37f540 100644 --- a/packages/next/src/structures/mixins/BaseChannelMixin.ts +++ b/packages/next/src/structures/mixins/BaseChannelMixin.ts @@ -3,6 +3,7 @@ import { equal } from 'node:assert'; import type { ChannelType } from '@discordjs/core'; import { kPatch, type Channel, type ChannelDataType } from '@discordjs/structures'; import type { Client } from '../../Client.js'; +import { container } from '../../util/container.js'; export interface BaseChannelMixin extends Channel {} @@ -10,12 +11,12 @@ export class BaseChannelMixin { declare public readonly client: Client; public async delete() { - await this.client['core'].api.channels.delete(this.id); + await container.get('client')['core'].api.channels.delete(this.id); return this; } public async fetch() { - const data = await this.client['core'].api.channels.get(this.id); + const data = await container.get('client')['core'].api.channels.get(this.id); equal(data.type, this.type); return this[kPatch](data as ChannelDataType); diff --git a/packages/next/src/structures/mixins/ChannelPermissionsMixin.ts b/packages/next/src/structures/mixins/ChannelPermissionsMixin.ts new file mode 100644 index 000000000000..9873bd12f6cd --- /dev/null +++ b/packages/next/src/structures/mixins/ChannelPermissionsMixin.ts @@ -0,0 +1,13 @@ +import type { ChannelType, GuildChannelType, ThreadChannelType } from '@discordjs/core'; +import { ChannelPermissionMixin as StructurePermissionMixin } from '@discordjs/structures'; + +export class ChannelPermissionsMixin< + Type extends Exclude = Exclude< + GuildChannelType, + ChannelType.GuildDirectory | ThreadChannelType + >, +> extends StructurePermissionMixin { + // public permissionsFor(userOrRole: RoleResolvable | UserResolvable) { + // return new PermissionsBitField(); + // } +} diff --git a/packages/next/src/util/cache.ts b/packages/next/src/util/cache.ts new file mode 100644 index 000000000000..d4c8031a78a9 --- /dev/null +++ b/packages/next/src/util/cache.ts @@ -0,0 +1,25 @@ +import { Collection } from '@discordjs/collection'; +import { kPatch, type Structure } from '@discordjs/structures'; +import type { Awaitable } from '@discordjs/util'; +import type { Snowflake } from 'discord-api-types/globals'; + +export interface StructureCache> { + add(key: Snowflake, data: Partial): Value; + get(key: Snowflake): Awaitable; + set(key: Snowflake, value: Value): Awaitable; +} + +export type CacheConstructor = new (...args: any[]) => StructureCache>; + +export class CollectionCache> + extends Collection + implements StructureCache +{ + public add(_key: Snowflake, _data: Partial): Value { + throw new Error('Method not implemented.'); + } + + public patch(key: Snowflake, data: Partial): Value { + return this.get(key)?.[kPatch](data) ?? this.add(key, data); + } +} diff --git a/packages/next/src/util/container.ts b/packages/next/src/util/container.ts new file mode 100644 index 000000000000..ca5aad126432 --- /dev/null +++ b/packages/next/src/util/container.ts @@ -0,0 +1,30 @@ +import { Collection } from '@discordjs/collection'; +import type { Client } from '../Client'; + +export class Container extends Collection {} + +export const container = new Container(); + +export interface Container { + get(key: Key): ContainerEntries[Key]; + get(key: string): undefined; + has(key: ContainerKey): true; + has(key: string): false; +} + +/** + * A type utility to get the keys of {@link ContainerEntries}. + */ +export type ContainerKey = keyof ContainerEntries; + +/** + * A type utility to get the values of {@link ContainerEntries}. + */ +export type ContainerValue = ContainerEntries[ContainerKey]; + +/** + * The {@link Container}'s registry, use module augmentation against this interface when adding new entries. + */ +export interface ContainerEntries { + client: Client; +} diff --git a/packages/next/src/util/types.ts b/packages/next/src/util/types.ts deleted file mode 100644 index e07c3d3d1f03..000000000000 --- a/packages/next/src/util/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Structure } from '@discordjs/structures'; -import type { Awaitable } from '@discordjs/util'; - -export interface Cache> { - get(key: Key): Awaitable; - set(key: Key, value: Type): Awaitable; -} - -export type CacheConstructor = new (...args: any[]) => Cache>; From f08146acc6acce6f0af244e57bf84156dadff3ee Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:10:21 +0200 Subject: [PATCH 6/6] fix: lint --- packages/discord.js/typings/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 28a7ab161d82..91eb0dcd64f9 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -66,7 +66,6 @@ import { APIMessageTopLevelComponent, APIMessageUserSelectInteractionData, APIModalComponent, - APIModalInteractionResponseCallbackComponent, APIModalInteractionResponseCallbackData, APIModalSubmitInteraction, APIOverwrite,